UNPKG

iobroker.js-controller

Version:

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

8,872 lines (8,101 loc) 409 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 // strictObjectChecks: flag which defaults to true - if true, adapter warns if states are set without an corresponding existing object const net = require('net'); const fs = require('fs-extra'); const extend = require('node.extend'); const util = require('util'); const os = require('os'); const EventEmitter = require('events').EventEmitter; const tools = require('./tools'); const pidUsage = require('pidusage'); const deepClone = require('deep-clone'); const EXIT_CODES = require('./exitCodes'); const {PluginHandler} = require('@iobroker/plugin-base'); const controllerVersion = require('../package.json').version; const password = require('./password'); const { FORBIDDEN_CHARS } = tools; const DEFAULT_SECRET = 'Zgfr56gFe87jJOM'; const ALIAS_STARTS_WITH = 'alias.'; const SYSTEM_ADMIN_USER = 'system.user.admin'; const SYSTEM_ADMIN_GROUP = 'system.group.administrator'; const QUALITY_SUBS_INITIAL = 0x20; const supportedFeatures = [ 'ALIAS', // Alias Feature supported, Since js-controller 2.0 'ALIAS_SEPARATE_READ_WRITE_ID', // Alias support separated ids for read and write, Since js-controller 3.0 'ADAPTER_GETPORT_BIND', // getPort method of adapter supports second parameter to bind to a special network interface, Since js-controller 2.0 'ADAPTER_DEL_OBJECT_RECURSIVE', // delObject supports options.recursive flag to delete objects structures recursive, Since js-controller 2.2 'ADAPTER_SET_OBJECT_SETS_DEFAULT_VALUE', // setObject(*) methods set the default (def) value via setState after the object is created. Since js-controller 2.0 'ADAPTER_AUTO_DECRYPT_NATIVE', // all native attributes, that are listed in an array `encryptedNative` in io-pack will be automatically decrypted and encrypted. Since js-controller 3.0 'PLUGINS', // configurable plugins supported. Since js-controller 3.0 'CONTROLLER_NPM_AUTO_REBUILD', // Automatic rebuild when node version mismatch is detected. Since js-controller 3.0 'CONTROLLER_READWRITE_BASE_SETTINGS' // If base settings could be read and written. Since js-controller 3.0 ]; //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_PERMISSION = 'permissionError'; /** * Look up the error description for an error code * * @param {number} code error code * @return {string} error description */ function getErrorText(code) { code = code || 0; return (EXIT_CODES[code] || code).toString(); } class Log { /** * @param {string} namespaceLog Logging namespace to prefix * @param {string} level The log level * @param {object} logger logger instance */ constructor(namespaceLog, level, logger) { this.namespaceLog = namespaceLog; this.level = level; // We have to bind the this context here or it is possible that `this` is // undefined when passing around the logger methods. This happens e.g. when doing this: // const log = new Log(...); // const test = log.info; // test(); this.logger = logger; this.silly = this.silly.bind(this); this.debug = this.debug.bind(this); this.info = this.info.bind(this); this.error = this.error.bind(this); this.warn = this.warn.bind(this); } silly(msg) { this.logger.silly(this.namespaceLog + ' ' + msg); } debug(msg) { this.logger.debug(this.namespaceLog + ' ' + msg); } info(msg) { this.logger.info(this.namespaceLog + ' ' + msg); } error(msg) { this.logger.error(this.namespaceLog + ' ' + msg); } warn(msg) { this.logger.warn(this.namespaceLog + ' ' + msg); } } /** * Adapter class * * How the initialization happens: * initObjects => initStates => prepareInitAdapter => createInstancesObjects => initAdapter => initLogging => ready * * @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); } /** @type {Record<string, any>} */ let config = null; let defaultObjs; const configFileName = tools.getConfigFileName(); if (fs.existsSync(configFileName)) { config = fs.readJSONSync(configFileName); config.states = config.states || {type: 'file'}; config.objects = config.objects || {type: 'file'}; } else { throw new Error(`Cannot find ${configFileName}`); } if (!options || (!config && !options.config)) { throw new Error('Configuration not set!'); } let schedule; let restartScheduleJob; let initializeTimeout; let adapterStates; let adapterObjects; let timers = {}; let timerId = 1; let intervals = {}; let stopInProgress = false; if (options.config && !options.config.log) { options.config.log = config.log; } config = options.config || config; this.startedInCompactMode = options.compact; const regUser = /^system\.user\./; const regGroup = /^system\.group\./; let firstConnection = true; let systemSecret = null; let reportInterval; this.logList = []; this.aliases = {}; this.aliasPatterns = []; // TODO: Remove this backward compatibility shim in the future this.objects = {}; this.eventLoopLags = []; this.overwriteLogLevel = false; this.adapterReady = false; // Provide tools for use in adapter this.tools = tools; // possible arguments // 0,1,.. - instance // info, debug, warn, error - log level // --force // --logs // --silent // --install // --debug = --force + --logs 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]; this.overwriteLogLevel = true; } 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 (process.argv[a] === '--debug') { config.forceIfDisabled = true; config.consoleOutput = true; if (config.log.level !== 'silly') { config.log.level = 'debug'; this.overwriteLogLevel = true; } } else if (process.argv[a] === '--console') { config.consoleOutput = 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'; config.log.noStdout = !config.consoleOutput; const logger = require('./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.name) { throw new Error('No name of adapter!'); } this.performStrictObjectChecks = options.strictObjectChecks !== false; this._getObjectsByArray = (keys, objects, options, cb, _index, _result, _errors) => { if (objects) { return tools.maybeCallbackWithError(cb, null, objects); } _index = _index || 0; _result = _result || []; _errors = _errors || []; while(!keys[_index] && _index < keys.length) { _index++; } if (_index >= keys.length) { return tools.maybeCallbackWithError(cb, _errors.find(e => e) ? _errors : null, _result); } // if empty => skip immediately this.getForeignObject(keys[_index], options, (err, obj) => { _result[_index] = obj; setImmediate(() => this._getObjectsByArray(keys, objects, options, cb, _index + 1, _result, _errors)); }); }; /** * 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 | number} [reason] optional termination description * @param {number} [exitCode] optional exit code */ this.terminate = (reason, exitCode) => { // This function must be defined very first, because in the next lines will be yet used. if (this.terminated) { return; } this.terminated = true; this.pluginHandler && this.pluginHandler.destroyAll(); if (reportInterval) { clearInterval(reportInterval); reportInterval = null; } if (restartScheduleJob) { restartScheduleJob.cancel(); restartScheduleJob = null; } if (typeof reason === 'number') { // Only the exit code was passed exitCode = reason; reason = null; } if (typeof exitCode !== 'number') { exitCode = process.argv.indexOf('--install') === -1 ? EXIT_CODES.ADAPTER_REQUESTED_TERMINATION : EXIT_CODES.NO_ERROR; } const isNotCritical = exitCode === EXIT_CODES.ADAPTER_REQUESTED_TERMINATION || exitCode === EXIT_CODES.START_IMMEDIATELY_AFTER_STOP || exitCode === EXIT_CODES.NO_ERROR ; const text = `${this.namespaceLog} Terminated (${getErrorText(exitCode)}): ${reason ? reason : 'Without reason'}`; if (isNotCritical) { logger.info(text); } else { logger.warn(text); } setTimeout(async () => { // give last states some time to get handled if (adapterStates) { try { await adapterStates.destroy(); } catch { // ignore } } if (adapterObjects) { try { await adapterObjects.destroy(); } catch { //ignore } } if (this.startedInCompactMode) { this.emit('exit', exitCode, reason); adapterStates = null; adapterObjects = null; } else { process.exit(exitCode === undefined ? EXIT_CODES.ADAPTER_REQUESTED_TERMINATION : exitCode); } }, 500); }; // 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(this.namespaceLog + ' Cannot find directory of adapter ' + options.name); this.terminate(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR); } } 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(this.namespaceLog + ' Cannot find directory of adapter ' + options.name); this.terminate(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR); } } } if (fs.existsSync(this.adapterDir + '/package.json')) { this.pack = fs.readJSONSync(this.adapterDir + '/package.json'); } else { logger.info(this.namespaceLog + ' Non npm module. No package.json'); } if (!this.pack || !this.pack.io) { if (fs.existsSync(this.adapterDir + '/io-package.json')) { this.ioPack = fs.readJSONSync(this.adapterDir + '/io-package.json'); } else { logger.error(this.namespaceLog + ' Cannot find: ' + this.adapterDir + '/io-package.json'); this.terminate(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR); } } else { this.ioPack = this.pack.io; } // If required system configuration. Store it in systemConfig attribute if (options.systemConfig) { this.systemConfig = config; // Workaround for an admin 5 issue which could lead to deleting the dataDir folder // TODO: remove it as soon as all adapters are fixed which use systemConfig.dataDir if (!Object.prototype.hasOwnProperty.call(this.systemConfig, 'dataDir')) { this.systemConfig.dataDir = tools.getDefaultDataDir(); } } let States; if (config.states && config.states.type) { try { States = require(`@iobroker/db-states-${config.states.type}`).Client; } catch (err) { throw new Error(`Unknown states type: ${config.states.type}: ${err.message}`); } } else { States = require('./states'); } let Objects; if (config.objects && config.objects.type) { try { Objects = require(`@iobroker/db-objects-${config.objects.type}`).Client; } catch (err) { throw new Error(`Unknown objects type: ${config.objects.type}: ${err.message}`); } } else { Objects = require('./objects'); } const ifaces = os.networkInterfaces(); const ipArr = []; for (const dev in ifaces) { if (!Object.prototype.hasOwnProperty.call(ifaces, dev)) { continue; } /*jshint loopfunc:true */ ifaces[dev].forEach(details => !details.internal && ipArr.push(details.address)); } const instance = parseInt(options.compactInstance !== undefined ? options.compactInstance : ((options.instance !== undefined) ? options.instance : (config.instance || 0)), 10); this.name = options.name; this.namespace = options.name + '.' + instance; this.namespaceLog = this.namespace + (this.startedInCompactMode ? ' (COMPACT)' : ' (' + process.pid + ')'); this._namespaceRegExp = new RegExp('^' + (this.namespace + '.').replace(/\./g, '\\.')); // cache the regex object 'adapter.0.' /** The cache of users */ this.users = {}; /** The cache of usernames */ this.usernames = {}; /** The cache of user groups */ this.groups = {}; this.defaultHistory = null; /** An array of instances, that support auto subscribe */ this.autoSubscribe = null; this.inputCount = 0; this.outputCount = 0; /** A RegExp to test for forbidden chars in object IDs */ this.FORBIDDEN_CHARS = FORBIDDEN_CHARS; /** Whether the adapter has already terminated */ this.terminated = false; let callbackId = 1; this.getPortRunning = null; /** * Checks if a passed ID is valid. Throws an error if id is invalid * * @param {string|object} id id to check or object with properties device, channel and state * @param {boolean} isForeignId true&false if the ID is a foreign/full ID or only an "adapter local" id * @param {object} options optional * @throws Error when id is invalid */ const validateId = (id, isForeignId, options) => { // there is special maintenance mode to clear the DB from invalid IDs if (options && options.maintenance && options.user === SYSTEM_ADMIN_USER) { return; } if (!id && id !== 0) { throw new Error('The id is empty! Please provide a valid id.'); } const type = typeof id; if (!isForeignId && type === 'number') { logger.warn(`${this.namespaceLog} The id "${id}" has an invalid type!: Expected "string" or "object", received "number".`); logger.warn(`${this.namespaceLog} This will be refused in future versions. Please report this to the developer.`); } else if (type !== 'string' && !tools.isObject(id)) { throw new Error(`The id "${id}" has an invalid type! Expected "string" or "object", received "${type}".`); } if (tools.isObject(id)) { // id can be an object, at least one of the following properties has to exist const reqProperties = ['device', 'channel', 'state']; let found = false; for (const reqProperty of reqProperties) { if (reqProperty !== undefined) { if (typeof reqProperty !== 'string') { throw new Error(`The id's property "${reqProperty}" of "${JSON.stringify(id)}" has an invalid type! Expected "string", received "${typeof reqProperty}".`); } if (reqProperty.includes('.')) { throw new Error(`The id's property "${reqProperty}" of "${JSON.stringify(id)}" contains the invalid character "."!`); } found = true; } } if (found === false) { throw new Error(`The id "${JSON.stringify(id)}" is an invalid object! Expected at least one of the properties "device", "channel" or "state" to exist.`); } } else { if (type !== 'string') { throw new Error(`The id "${JSON.stringify(id)}" has an invalid type! Expected "string", received "${type}".`); } if (id.endsWith('.')) { throw new Error(`The id "${id}" is invalid. Ids are not allowed to end in "."`); } } }; /** * 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('Following port is free: ' + port); * }); * </code></pre> * * @alias getPort * @memberof Adapter * @param {number} port port number to start the search for free port * @param {string} [host] optional hostname for the port search * @param {(port: number) => void} callback return result * <pre><code>function (port) {}</code></pre> */ this.getPort = (port, host, callback) => { if (!port) { throw new Error('adapterGetPort: no port'); } if (typeof host === 'function') { callback = host; host = null; } if (!host) { host = undefined; } if (typeof port === 'string') { port = parseInt(port, 10); } this.getPortRunning = {port, host, callback}; const server = net.createServer(); try { server.listen({port, host},(/* err */) => { server.once('close', () => { return tools.maybeCallback(callback, port); }); server.close(); }); server.on('error', (/* err */) => { setTimeout(() => this.getPort(port + 1, host, callback), 100); }); } catch { setImmediate(() => this.getPort(port + 1, host, callback)); } }; /** * Promise-version of Adapter.getPort */ this.getPortAsync = tools.promisifyNoError(this.getPort, this); /** * Method to check for available Features for adapter development * * Use it like ... * <pre><code> * if (adapter.supportsFeature && adapter.supportsFeature('ALIAS')) { * ... * } * </code></pre> * @alias supportsFeature * @memberof Adapter * @param {string} featureName the name of the feature to check * @returns {boolean} true/false wether the featufre is in the list of supported features */ this.supportsFeature = featureName => { return supportedFeatures.includes(featureName); }; /** * 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 {(success: boolean, user: string) => void} callback return result * <pre><code> * function (result) { * if (result) adapter.log.debug('User is valid'); * } * </code></pre> */ this.checkPassword = async (user, pw, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!callback) { throw new Error('checkPassword: no callback'); } if (user && !regUser.test(user)) { // its not yet a `system.user.xy` id, thus we assume it's a username if (!this.usernames[user]) { // we did not find the id of the username in our cache -> update cache try { await updateUsernameCache(); } catch (e) { this.log.error(e.message); } if (!this.usernames[user]) { // user still not there, its no valid user -> fallback to legacy check user = `system.user.${user.toString().replace(this.FORBIDDEN_CHARS, '_').replace(/\s/g, '_').replace(/\./g, '_').toLowerCase()}`; } else { user = this.usernames[user].id; } } else { user = this.usernames[user].id; } } this.getForeignObject(user, options, (err, obj) => { if (err || !obj || !obj.common || (!obj.common.enabled && user !== SYSTEM_ADMIN_USER)) { return tools.maybeCallback(callback, false, user); } else { password(pw).check(obj.common.password, (err, res) => { return tools.maybeCallback(callback, res, user); }); } }); }; /** * Promise-version of Adapter.checkPassword */ this.checkPasswordAsync = tools.promisifyNoError(this.checkPassword, this); /** * Return ID of given username * * @param {string} username - name of the user * @return {Promise<undefined|string>} */ this.getUserID = async username => { if (!this.usernames[username]) { try { // did not find username, we should have a look in the cache await updateUsernameCache(); if (!this.usernames[username]) { return; } } catch (e) { this.log.error(e.message); return; } } return this.usernames[username].id; }; /** * 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 {ioBroker.ErrorCallback} [callback] return result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot set password: ' + err); * } * </code></pre> */ this.setPassword = async (user, pw, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (user && !regUser.test(user)) { // its not yet a `system.user.xy` id, thus we assume it's a username if (!this.usernames[user]) { // we did not find the id of the username in our cache -> update cache try { await updateUsernameCache(); } catch (e) { this.log.error(e); } if (!this.usernames[user]) { // user still not there, fallback to legacy check user = `system.user.${user.toString().replace(this.FORBIDDEN_CHARS, '_').replace(/\s/g, '_').replace(/\./g, '_').toLowerCase()}`; } else { user = this.usernames[user].id; } } else { user = this.usernames[user].id; } } this.getForeignObject(user, options, (err, obj) => { if (err || !obj) { return tools.maybeCallbackWithError(callback, 'User does not exist'); } // BF: (2020.05.22) are the empty passwords allowed?? if (!pw) { this.extendForeignObject(user, { common: { password: '' } }, options, () => { return tools.maybeCallback(callback); }); } else { password(pw).hash(null, null, (err, res) => { if (err) { return tools.maybeCallbackWithError(callback, err); } this.extendForeignObject(user, { common: { password: res } }, options, () => { return tools.maybeCallbackWithError(callback, null); }); }); } }); }; /** * Promise-version of Adapter.setPassword */ this.setPasswordAsync = tools.promisify(this.setPassword, this); /** * 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 {(result: boolean) => void} callback return result * <pre><code> * function (result) { * if (result) adapter.log.debug('User exists and in the group'); * } * </code></pre> */ this.checkGroup = async (user, group, options, callback) => { user = (user || ''); if (typeof options === 'function') { callback = options; options = null; } if (user && !regUser.test(user)) { // its not yet a `system.user.xy` id, thus we assume it's a username if (!this.usernames[user]) { // we did not find the id of the username in our cache -> update cache try { await updateUsernameCache(); } catch (e) { this.log.error(e); } if (!this.usernames[user]) { // user still not there, its no valid user -> fallback user = `system.user.${user.toString().replace(this.FORBIDDEN_CHARS, '_').replace(/\s/g, '_').replace(/\./g, '_').toLowerCase()}`; } else { user = this.usernames[user].id; } } else { user = this.usernames[user].id; } } if (group && !regGroup.test(group)) { group = 'system.group.' + group; } this.getForeignObject(user, options, (err, obj) => { if (err || !obj) { return tools.maybeCallback(callback, false); } this.getForeignObject(group, options, (err, obj) => { if (err || !obj) { return tools.maybeCallback(callback, false); } if (obj.common.members.indexOf(user) !== -1) { return tools.maybeCallback(callback, true); } else { return tools.maybeCallback(callback, false); } }); }); }; /** * Promise-version of Adapter.checkGroup */ this.checkGroupAsync = tools.promisifyNoError(this.checkGroup, this); /** @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'}, * chownFile: {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 {(result: ioBroker.PermissionSet) => void} [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> */ this.calculatePermissions = async (user, commandsPermissions, options, callback) => { user = (user || ''); if (typeof options === 'function') { callback = options; options = null; } if (user && !regUser.test(user)) { // its not yet a `system.user.xy` id, thus we assume it's a username if (!this.usernames[user]) { // we did not find the id of the username in our cache -> update cache try { await updateUsernameCache(); } catch (e) { this.log.error(e.message); } // user still not there, fallback if (!this.usernames[user]) { user = `system.user.${user.toString().replace(this.FORBIDDEN_CHARS, '_').replace(/\s/g, '_').replace(/\./g, '_').toLowerCase()}`; } else { user = this.usernames[user].id; } } else { user = this.usernames[user].id; } } // read all groups let acl = {user: user}; if (user === SYSTEM_ADMIN_USER) { acl.groups = [SYSTEM_ADMIN_GROUP]; for (const c of Object.keys(commandsPermissions)) { if (!commandsPermissions[c].type) { continue; } acl[commandsPermissions[c].type] = acl[commandsPermissions[c].type] || {}; acl[commandsPermissions[c].type][commandsPermissions[c].operation] = true; } return tools.maybeCallback(callback, acl); } acl.groups = []; this.getForeignObjects('*', 'group', null, options, (err, groups) => { // aggregate all groups permissions, where this user is if (groups) { for (const g of Object.keys(groups)) { 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_ADMIN_GROUP) { 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 of Object.keys(gAcl)) { // fix bug. Some version have user instead of users. if (type === 'user') { acl.users = acl.users || {}; } else { acl[type] = acl[type] || {}; } for (const op of Object.keys(gAcl[type])) { // 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) { logger.error(this.namespaceLog + ' Cannot set acl: ' + e); logger.error(this.namespaceLog + ' Cannot set acl: ' + JSON.stringify(gAcl)); logger.error(this.namespaceLog + ' Cannot set acl: ' + JSON.stringify(acl)); } } } } return tools.maybeCallback(callback, acl); }); }; /** * Promise-version of Adapter.calculatePermissions */ this.calculatePermissionsAsync = tools.promisifyNoError(this.calculatePermissions, this); const readFileCertificate = cert => { if (typeof cert === 'string') { try { // if length < 1024 its no valid cert, so we assume a path to a valid certificate if (cert.length < 1024 && fs.existsSync(cert)) { const certFile = cert; cert = fs.readFileSync(certFile, 'utf8'); // start watcher of this file fs.watch(certFile, (eventType, filename) => { logger.warn(`${this.namespaceLog} New certificate "${filename}" detected. Restart adapter`); setTimeout(stop, 2000, false, true); }); } } catch (e) { logger.error(`${this.namespaceLog} Could not read certificate from file ${cert}: ${e.message}`); } } 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 {(err: string | null, certs?: ioBroker.Certificates, useLetsEncryptCert?: boolean) => void} callback return result * <pre><code> * function (err, certs, letsEncrypt) { * adapter.log.debug('private key: ' + certs.key); * adapter.log.debug('public cert: ' + certs.cert); * adapter.log.debug('chained cert: ' + certs.ca); * } * </code></pre> */ this.getCertificates = (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 || this.config.certPublic; privateName = privateName || this.config.certPrivate; chainedName = chainedName || this.config.certChained; // Load certificates this.getForeignObject('system.certificates', null, (err, obj) => { if (err || !obj || !obj.native.certificates || !publicName || !privateName || !obj.native.certificates[publicName] || !obj.native.certificates[privateName] || (chainedName && !obj.native.certificates[chainedName]) ) { logger.error(this.namespaceLog + ' Cannot configure secure web server, because no certificates found: ' + publicName + ', ' + privateName + ', ' + chainedName); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_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)/g, '').trim()) { ca.push(chained[c] + '-----END CERTIFICATE-----\r\n'); } } } return tools.maybeCallbackWithError(callback, null, { key: readFileCertificate(obj.native.certificates[privateName]), cert: readFileCertificate(obj.native.certificates[publicName]), ca }, obj.native.letsEncrypt); } }); }; /** * Promise-version of Adapter.getCertificates */ this.getCertificatesAsync = tools.promisify(this.getCertificates, this); /** * Restarts an instance of the adapter. * * @memberof Adapter */ this.restart = () => { logger.warn(this.namespaceLog + ' Restart initiated'); this.stop(); }; /** * 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 */ this.updateConfig = newConfig => { // merge the old and new configuration const _config = Object.assign({}, this.config, newConfig); // update the adapter config object const configObjId = `system.adapter.${this.namespace}`; this.getForeignObjectAsync(configObjId) .then(obj => { if (!obj) { return Promise.reject(new Error(tools.ERRORS.ERROR_DB_CLOSED)); } obj.native = _config; return this.setForeignObjectAsync(configObjId, obj); }) .catch(err => logger.error(`${this.namespaceLog} Updating the adapter config failed: ${err.message}`)) ; }; /** * Disables and stops the adapter instance. */ this.disable = () => { // update the adapter config object const configObjId = `system.adapter.${this.namespace}`; this.getForeignObjectAsync(configObjId) .then(obj => { if (!obj) { return Promise.reject(new Error(tools.ERRORS.ERROR_DB_CLOSED)); } obj.common.enabled = false; return this.setForeignObjectAsync(configObjId, obj); }) .catch(err => logger.error(`${this.namespaceLog} Disabling the adapter instance failed: ${err.message}`)) ; }; /** * Reads the encrypted parameter from config. * * It returns promise if no callback is provided. * @param {string} attribute - attribute name in native configuration part * @param {(error: Error | null | undefined, result?: string) => void} [callback] - optional callback * @returns {Promise<any>} promise if no callback provided * */ this.getEncryptedConfig = async (attribute, callback) => { if (Object.prototype.hasOwnProperty.call(this.config, attribute)) { if (systemSecret !== null) { return tools.maybeCallbackWithError(callback, null, tools.decrypt(systemSecret, this.config[attribute])); } else { try { const data = await this.getForeignObjectAsync('system.config'); if (data && data.native) { systemSecret = data.native.secret; } } catch { // do nothing - we initialize default secret below } systemSecret = systemSecret || DEFAULT_SECRET; return tools.maybeCallbackWithError(callback, null, tools.decrypt(systemSecret, this.config[attribute])); } } else { return tools.maybeCallbackWithError(callback, `Attribute "${attribute}" not found`); } }; /** * Same as setTimeout * but it check the running timers on unload * * It returns promise if no callback is provided. * @param {function} cb - timer callback * @param {number} timeout - timeout in milliseconds * @param {any[]} args - as many arguments as needed, which will be passed to setTimeout * @returns {number|void} timer id */ this.setTimeout = (cb, timeout, ...args) => { if (typeof cb !== 'function') { this.log.warn(`setTimeout expected callback to be of type "function", but got "${typeof cb}"`); return; } timerId = (timerId + 1) % 0xFFFFFFFF; const _cb = (function (id, _cb) { return function (...args) { delete timers[id]; _cb(...args); }; })(timerId, cb); timers[timerId] = setTimeout.call(null, _cb, timeout, ...args); return timerId; }; /** * Same as clearTimeout * but it check the running timers on unload * * It returns promise if no callback is provided. * @param {number} id - timer id */ this.clearTimeout = id => { if (!timers[id]) { logger.warn(`${this.namespaceLog} Clear yet terminated timer ${id}`); } else { clearTimeout(timers[id]); delete timers[id]; } }; /** * Same as setInterval * but it check the running intervals on unload * * It returns promise if no callback is provided. * @param {function} cb - interval callback * @param {number} timeout - interval in milliseconds * @param {any[]} args - as many arguments as needed, which will be passed to setTimeout * @returns {number|void} interval id */ this.setInterval = (cb, timeout, ...args) => { if (typeof cb !== 'function') { this.log.error(`setInterval expected callback to be of type "function", but got "${typeof cb}"`); return; } timerId = (timerId + 1) % 0xFFFFFFFF; intervals[timerId] = setInterval.call(null, cb, timeout, ...args); return timerId; }; /** * Same as clearInterval * but it check the running intervals on unload * * It returns promise if no callback is provided. * @param {number} id - interval id */ this.clearInterval = id => { if (!intervals[id]) { logger.warn(`${this.namespaceLog} Clear yet terminated interval ${id}`); } else { clearInterval(intervals[id]); delete intervals[id]; } }; // Can be later deleted if no more appears this.inited = false; const extendObjects = async (tasks, callback) => { if (!tasks || !tasks.length) { return tools.maybeCallback(callback); } const task = tasks.shift(); const state = task.state; if (state !== undefined) { delete task.state; } try { tools.validateGeneralObjectProperties(task, true); } catch (e) { // todo: in the future we will not create this object logger.warn(`${this.namespaceLog} Object ${task._id} is invalid: ${e.message}`); logger.warn(`${this.namespaceLog} This object will not be created in future versions. Please report this to the developer.`); } if (!adapterObjects) { logger.info(`${this.namespaceLog} extendObjects not processed because Objects database not connected.`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } const options = {preserve: {common: ['name']}}; try { await this.extendForeignObject(task._id, task, options); } catch { // ignore } if (state !== undefined) { if (!adapterStates) { logger.info(`${this.namespaceLog} extendObjects not processed because States database not connected.`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(task._id, { val: state, from: `system.adapter.${this.namespace}`, ack: true }, () => setImmediate(extendObjects, tasks, callback)); } else { setImmediate(extendObjects, tasks, callback); } }; const createInstancesObjects = (instanceObj, callback) => { let objs; if (instanceObj && instanceObj.common && !instanceObj.common.onlyWWW && instanceObj.common.mode !== 'once') { objs = tools.getInstanceIndicatorObjects(this.namespace, instanceObj.common.wakeup); } else { objs = []; } if (instanceObj && instanceObj.instanceObjects) { instanceObj.instanceObjects.forEach(async obj => { if (!obj._id.startsWith(this.namespace)) { // instanceObjects are normally defined without namespace prefix obj._id = (obj._id === '') ? this.namespace : this.namespace + '.' + obj._id; } if (obj && (obj._id || obj.type === 'meta')) { if (obj.common) { if (obj.common.name) { // if name has many languages if (typeof obj.common.name === 'object') { Object.keys(obj.common.name).forEach(lang => obj.common.name[lang] = obj.common.name[lang].replace('%INSTANCE%', instance)); } else { obj.common.name = obj.common.name.replace('%INSTANCE%', instance); } } if (obj.common.desc) { // if description has many languages if (typeof obj.common.desc === 'object') { Object.keys(obj.common.desc).forEach(lang => obj.common.desc[lang] = obj.common.desc[lang].replace('%INSTANCE%', instance)); } else { obj.common.desc = obj.common.desc.replace('%INSTANCE%', instance); } } if (obj.type === 'state' && obj.common.def !== undefined) { // default value given - if obj non existing we have to set it try { const checkObj = await this.getForeignObjectAsync(obj._id); if (!checkObj) { obj.state = obj.common.def; } } catch (e) { logger.warn(`${this.namespaceLog} Did not add default (${obj.common.def}) value on creation of ${obj._id}: ${e.message}`); } } } objs.push(obj); } else { logger.error(this.namespaceLog + ' ' + options.name + '.' + instance + ' invalid instance object: ' + JSON.stringify(obj)); } }); } extendObjects(objs, callback); }; const prepareInitAdapter = () => { if (options.instance !== undefined) { initAdapter(options); } else { this.getForeignState(`system.adapter.${this.namespace}.alive`, null, (err, resAlive) => { this.getForeignState(`system.adapter.${this.namespace}.sigKill`, null, (err, killRes) => { if (killRes && killRes.val !== undefined) { killRes.val = parseInt(killRes.val, 10); } if (!config.isInstall && this.startedInCompactMode && killRes && !killRes.ack && killRes.val === -1) { logger.error(this.namespaceLog + ' ' + options.name + '.' + instance + ' needs to be stopped because not correctly started in compact mode'); this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); } else if (!config.forceIfDisabled && !config.isInstall && !this.startedInCompactMode && killRes && killRes.from && killRes.from.startsWith('system.host.') && killRes.ack && !isNaN(killRes.val) && killRes.val !== process.pid) { logger.error(this.namespaceLog + ' ' + options.name + '.' + instance + ' invalid process id scenario ' + killRes.val + ' vs. own ID ' + process.pid + '. Stopping'); this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); } else if (!config.isInstall && resAlive && resAlive.val === true && resAlive.ack && !config.forceIfDisabled) { logger.error(this.namespaceLog + ' ' + options.name + '.' + instance + ' already running'); this.terminate(EXIT_CODES.ADAPTER_ALREADY_RUNNING); } else { this.getForeignObject('system.adapter.' + this.namespace, null, (err, res) => { if ((err || !res) && !config.isInstall) { logger.error(this.namespaceLog + ' ' + options.name + '.' + instance + ' invalid config'); this.terminate(EXIT_CODES.INVALID_ADAPTER_CONFIG); } else { createInstancesObjects(res, () => initAdapter(res)); } }); } }); }); } }; const autoSubscribeOn = async () => { if (!this.autoSubscribe && adapterObjects) { try { // collect all const res = await adapterObjects.getObjectViewAsync('system', 'instance', { startkey: 'system.adapter.', endkey: 'system.adapter.\u9999' }); if (res && res.rows) { this.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); // cut system.adapter. if (this.autoSubscribe.indexOf(_id) === -1) { this.autoSubscribe.push(_id); } } } } // because of autoSubscribe await adapterObjects.subscribeAsync('system.adapter.*'); } catch { // ignore } } }; const initObjects = cb => { initializeTimeout = setTimeout(() => { initializeTimeout = null; if (config.isInstall) { logger && logger.warn(this.namespaceLog + ' no connection to objects DB. Terminating'); this.terminate(EXIT_CODES.NO_ERROR); } else { logger && logger.warn(this.namespaceLog + ' slow connection to objects DB. Still waiting ...'); } }, (config.objects.connectTimeout || 2000) * 3); // Because we do not connect only anymore, give it a bit more time adapterObjects = new Objects({ namespace: this.namespaceLog, connection: config.objects, logger: logger, connected: () => { this.connected = true; if (initializeTimeout) { clearTimeout(initializeTimeout); initializeTimeout = null; } // subscribe to user changes adapterObjects.subscribe('system.user.*'); // Read dateformat if using of formatDate is announced if (options.useFormatDate) { this.getForeignObject('system.config', (err, data) => { if (data && data.common) { this.dateFormat = data.common.dateFormat; this.isFloatComma = data.common.isFloatComma; this.language = data.common.language; this.longitude = data.common.longitude; this.latitude = data.common.latitude; this.defaultHistory = data.common.defaultHistory; } if (data && data.native) { systemSecret = data.native.secret; } return tools.maybeCallback(cb); }); } else { return tools.maybeCallback(cb); } }, disconnected: () => { this.connected = false; !this.terminated && setTimeout(() => { if (this.connected) { return; } // If reconnected in the meantime, do not terminate logger && logger.warn(this.namespaceLog + ' Cannot connect/reconnect to objects DB. Terminating'); this.terminate(EXIT_CODES.NO_ERROR); }, 4000); }, change: async (id, obj) => { // System level object changes (and alias objects) if (obj === 'null' || obj === '') { obj = null; } if (!id) { logger.error(this.namespaceLog + ' change ID is empty: ' + JSON.stringify(obj)); return; } // If desired, that adapter must be terminated if (id === `system.adapter.${this.namespace}` && obj && obj.common && obj.common.enabled === false) { logger.info(this.namespaceLog + ' Adapter is disabled => stop'); stop(); setTimeout(() => this.terminate(EXIT_CODES.NO_ERROR), 4000); return; } // update language, dateFormat and comma if (id === 'system.config' && obj && obj.common && (options.useFormatDate || this.defaultHistory !== undefined)) { this.dateFormat = obj.common.dateFormat; this.isFloatComma = obj.common.isFloatComma; this.language = obj.common.language; this.longitude = obj.common.longitude; this.latitude = obj.common.latitude; this.defaultHistory = obj.common.defaultHistory; } // if alias if (id.startsWith(ALIAS_STARTS_WITH)) { // aliases['sourceId'] = { // source: {common attributes}, // targets: [ // { // alias: {}, // id: 'aliasId', // pattern: 'some pattern', // type: stateType, // max: number, // min: number, // } // ] // }; // if this.aliases is empty, or no target found its a new alias let isNewAlias = true; Object.keys(this.aliases).forEach(sourceId => { const alias = this.aliases[sourceId]; const targetAlias = alias.targets.find(entry => entry.id === id); // Find entry for this alias if (targetAlias) { isNewAlias = false; // new sourceId or same if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // check if id.read or id const newSourceId = typeof obj.common.alias.id.read === 'string' ? obj.common.alias.id.read : obj.common.alias.id; // if linked ID changed if (newSourceId !== sourceId) { this._removeAliasSubscribe(sourceId, targetAlias, async () => { try { await this._addAliasSubscribe(obj, targetAlias.pattern); } catch (e) { logger.error(`${this.namespaceLog} Could not add alias subscription: ${e.message}`); } }); } else { // update attributes targetAlias.min = obj.common.min; targetAlias.max = obj.common.max; targetAlias.type = obj.common.type; targetAlias.alias = deepClone(obj.common.alias); } } else { // link was deleted // remove from targets array this._removeAliasSubscribe(sourceId, targetAlias); } } }); // it's a new alias, we add it to our subscription if (isNewAlias) { for (const aliasPattern of this.aliasPatterns) { // check if its in our subs range, if so add it const testPattern = (aliasPattern.slice(-1) === '*') ? new RegExp(tools.pattern2RegEx(aliasPattern)) : aliasPattern; if (typeof testPattern === 'string' && aliasPattern === id || testPattern instanceof RegExp && testPattern.test(id)) { try { await this._addAliasSubscribe(obj, id); } catch (e) { this.log.warn(`Could not add alias subscription: ${e.message}`); } break; } } } } // process autosubscribe adapters if (id.startsWith('system.adapter.')) { if (obj && obj.common && obj.common.subscribable) { const _id = id.substring(15); // 'system.adapter.'.length if (obj.common.enabled) { if (this.autoSubscribe.indexOf(_id) === -1) { this.autoSubscribe.push(_id); } } else { const pos = this.autoSubscribe.indexOf(_id); if (pos !== -1) { this.autoSubscribe.splice(pos, 1); } } } } // Clear cache if got the message about change (Will work for admin and javascript - TODO: maybe always subscribe?) if (id.match(/^system\.user\./) || id.match(/^system\.group\./)) { this.users = {}; this.groups = {}; this.usernames = {}; } }, changeUser: (id, obj) => { // User level object changes if (obj === 'null' || obj === '') { obj = null; } if (!id) { logger.error(`${this.namespaceLog} change ID is empty: ${JSON.stringify(obj)}`); return; } // remove protectedNative if not admin or own adapter const adapterName = this.namespace.split('.')[0]; if (obj && obj.protectedNative && obj.protectedNative.length && obj._id && obj._id.startsWith('system.adapter.') && adapterName !== 'admin' && adapterName !== obj._id.split('.')[2] ) { for (const attr of obj.protectedNative) { delete obj.native[attr]; } } if (this.adapterReady) { // update oObjects structure if desired if (this.oObjects) { if (obj) { this.oObjects[id] = obj; } else { delete this.oObjects[id]; } } if (!stopInProgress) { typeof options.objectChange === 'function' && setImmediate(options.objectChange, id, obj); // emit 'objectChange' event instantly setImmediate(() => this.emit('objectChange', id, obj)); } } } }); /** * Performs the strict object check, which includes checking object existence, read-only logic, type and min/max * additionally it rounds state values whose objects have a common.step attribute defined * * @param {string} id - id of the state * @param {object} state - ioBroker setState object * @return {Promise<void>} */ this._performStrictObjectCheck = async (id, state) => { // TODO: in js-c 3.5 (or 2 releases after 3.3) we should let it throw and add tests, maybe we // can already let the non existing object case throw with 3.4 because this is already producing a warning try { if (state.val === undefined) { // only ack etc. is also possible to acknowledge the current value, // if only undefined provided, it should have thrown before return; } const obj = await adapterObjects.getObjectAsync(id); // at first check object existence if (!obj) { logger.warn(`${this.namespaceLog} State "${id}" has no existing object, this might lead to an error in future versions`); return; } // for a state object we require common.type to exist if (obj.common && obj.common.type) { // check if we are allowed to write (read-only can only be written with ack: true) if (!state.ack && obj.common.write === false) { logger.warn(`${this.namespaceLog} Read-only state "${id}" has been written without ack-flag with value "${state.val}"`); } if (state.val !== null) { // now check if type is correct, null is always allowed // types can be 'number', 'string', 'boolean', 'array', 'object', 'mixed', 'file', 'json' // array, object, file, json need to be string if (!(obj.common.type === 'mixed' && typeof state.val !== 'object' || obj.common.type !== 'object' && obj.common.type === typeof state.val || obj.common.type === 'array' && typeof state.val === 'string' || obj.common.type === 'json' && typeof state.val === 'string' || obj.common.type === 'file' && typeof state.val === 'string' || obj.common.type === 'object' && typeof state.val === 'string') ) { if (['object', 'json', 'file', 'array'].includes(obj.common.type)) { logger.info(`${this.namespaceLog} State value to set for "${id}" has to be stringified but received type "${typeof state.val}"`); } else { logger.info(`${this.namespaceLog} State value to set for "${id}" has to be ${obj.common.type === 'mixed' ? `one of type "string", "number", "boolean"` : `type "${obj.common.type}"`} but received type "${typeof state.val}" `); } } // now round step and check min/max if it's a number if (typeof state.val === 'number') { if (typeof obj.common.step === 'number' && obj.common.step > 0) { // round to next step const inv = 1 / obj.common.step; state.val = Math.round(state.val * inv) / inv; } if (obj.common.max !== undefined && state.val > obj.common.max) { logger.warn(`${this.namespaceLog} State value to set for "${id}" has value "${state.val}" greater than max "${obj.common.max}"`); } if (obj.common.min !== undefined && state.val < obj.common.min) { logger.warn(`${this.namespaceLog} State value to set for "${id}" has value "${state.val}" less than min "${obj.common.min}"`); } } } } else { logger.warn(`${this.namespaceLog} Object of state "${id}" is missing the required property "common.type"`); } } catch (e) { logger.warn(`${this.namespaceLog} Could not perform strict object check of state ${id}: ${e.message}`); } }; /** * @param {string | {device?: string, channel?: string, state?: string}} id * @param {boolean} [isPattern=false] */ this._fixId = (id, isPattern/* , type */) => { if (!id) { id = ''; } let result = ''; // If id is an object if (tools.isObject(id)) { // Add namespace + device + channel result = this.namespace + '.' + (id.device ? id.device + '.' : '') + (id.channel ? id.channel + '.' : '') + (id.state ? id.state : ''); } else if (typeof id === 'string') { result = id; // if not instance name itself and also not starts with namespace and "." if (id !== this.namespace && !this._namespaceRegExp.test(id)) { if (!isPattern) { result = this.namespace + (id ? '.' + id : ''); } else { result = this.namespace + '.' + (id ? id : ''); } } } return result; }; /** * Helper method for `set[Foreign]Object[NotExists]` that also sets the default value if one is configured * @param {string} id of the object * @param obj The object to set * @param {object} [options] * @param {function} [callback] */ const setObjectWithDefaultValue = async (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = undefined; } if (!adapterObjects) { this.log.info('setObject not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { tools.validateGeneralObjectProperties(obj, false); } catch (e) { // todo: in the future we will not create this object this.log.warn(`Object ${id} is invalid: ${e.message}`); this.log.warn('This object will not be created in future versions. Please report this to the developer.'); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } try { const result = await adapterObjects.setObjectAsync(id, obj, options); if (obj.type === 'state' && obj.common && obj.common.def !== undefined && obj.common.def !== null) { const state = await this.getForeignStateAsync(id); // only set the def state, if state is non-existent if (!state || state.val === undefined) { await this.setForeignStateAsync(id, {val: obj.common.def, q: QUALITY_SUBS_INITIAL, ack: true}); } } return tools.maybeCallbackWithError(callback, null, result); } catch (e) { return tools.maybeCallbackWithError(callback, e); } }; /** * 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/ioBroker/ioBroker/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 {ioBroker.SetObjectCallback} [callback] return result * @returns {ioBroker.SetObjectPromise} * <pre><code> * function (err, obj) { * // obj is {id: id} * if (err) adapter.log.error('Cannot write object: ' + err); * } * </code></pre> */ this.setObject = (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!defaultObjs) { defaultObjs = require('./defaultObjs.js')('de', '°C', 'EUR'); } if (!obj) { logger.error(`${this.namespaceLog} setObject: try to set null object for ${id}`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_EMPTY_OBJECT); } if (!tools.isObject(obj)) { logger.error(`${this.namespaceLog} setForeignObject: type of object parameter expected to be an object, but "${typeof obj}" provided`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_NO_OBJECT); } if (obj.type !== 'meta') { try { validateId(id, false, null); } catch (err) { logger.error(tools.appendStackTrace(`${this.namespaceLog} ${err.message}`)); return; } } if (Object.prototype.hasOwnProperty.call(obj, 'type')) { if (!Object.prototype.hasOwnProperty.call(obj,'native')) { logger.warn(this.namespaceLog + ' setObject ' + id + ' (type=' + obj.type + ') property native missing!'); obj.native = {}; } // Check property 'common' if (!Object.prototype.hasOwnProperty.call(obj, 'common')) { logger.warn(this.namespaceLog + ' 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 (Object.prototype.hasOwnProperty.call(obj.common, 'role') && defaultObjs[obj.common.role]) { obj.common = extend(true, {}, defaultObjs[obj.common.role], obj.common); } else if (!Object.prototype.hasOwnProperty.call(obj.common, 'role')) { logger.warn(`${this.namespaceLog} setObject ${id} (type=${obj.type}) property common.role missing!`); } if (!Object.prototype.hasOwnProperty.call(obj.common, 'type')) { logger.warn(`${this.namespaceLog} setObject ${id} (type=${obj.type}) property common.type missing!`); } if (Object.prototype.hasOwnProperty.call(obj.common, 'custom') && obj.common.custom !== null && !tools.isObject(obj.common.custom)) { logger.error(`${this.namespaceLog} setObject ${id} (type=${obj.type}) property common.custom is of type ${typeof obj.common.custom}, expected object.`); return tools.maybeCallbackWithError(callback, 'common.custom needs to be an object'); } } else { if (Object.prototype.hasOwnProperty.call(obj.common, 'custom') && obj.common.custom !== null) { logger.warn(`${this.namespaceLog} setObject ${id} (type=${obj.type}) property common.custom must not exist.`); delete obj.common.custom; } } if (!Object.prototype.hasOwnProperty.call(obj.common, 'name')) { obj.common.name = id; logger.debug(this.namespaceLog + ' setObject ' + id + ' (type=' + obj.type + ') property common.name missing, using id as name'); } id = this._fixId(id, false/*, obj.type*/); if (obj.children || obj.parent) { logger.warn(this.namespaceLog + ' Do not use parent or children for ' + id); } obj.from = obj.from || ('system.adapter.' + this.namespace); obj.user = obj.user || (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = obj.ts || Date.now(); setObjectWithDefaultValue(id, obj, options, callback); } else { logger.error(this.namespaceLog + ' setObject ' + id + ' mandatory property type missing!'); return tools.maybeCallbackWithError(callback, 'mandatory property type missing!'); } }; /** * Promise-version of Adapter.setObject */ this.setObjectAsync = tools.promisify(this.setObject, this); /** * Get all states, channels and devices of this adapter. * * @alias getAdapterObjects * @memberof Adapter * @param {(objects: Record<string, ioBroker.Object>) => void} callback return result * <pre><code> * function (objects) { * for (var id in objects) { * adapter.log.debug(id); * } * } * </code></pre> */ this.getAdapterObjects = async callback => { const ret = {}; // Adds result rows to the return object /** @param {any[] | undefined} rows */ const addRows = rows => { if (rows) { for (const {id, value} of rows) { ret[id] = value; } } }; if (!adapterObjects) { return tools.maybeCallback(callback, ret); } const options = {startkey: this.namespace + '.', endkey: this.namespace + '.\u9999', include_docs: true}; try { const folders = await adapterObjects.getObjectViewAsync('system', 'folder', options); addRows(folders.rows); } catch { /* ignore, we'll return what we get till now */ } try { const devices = await adapterObjects.getObjectViewAsync('system', 'device', options); addRows(devices.rows); } catch { /* ignore, we'll return what we get till now */ } try { const channels = await adapterObjects.getObjectViewAsync('system', 'channel', options); addRows(channels.rows); } catch { /* ignore, we'll return what we get till now */ } try { const states = await adapterObjects.getObjectViewAsync('system', 'state', options); addRows(states.rows); } catch { /* ignore, we'll return what we get till now */ } return tools.maybeCallback(callback, ret); }; /** * Promise-version of Adapter.getAdapterObjects */ this.getAdapterObjectsAsync = tools.promisifyNoError(this.getAdapterObjects, this); /** * Extend some object and create it if it does not exist * * You can change or extend some object. E.g existing object is: * <pre><code> * { * common: { * name: 'Adapter name', * desc: 'Description' * }, * type: 'state', * native: { * unused: 'text' * } * } * </code></pre> * * If following object will be passed as argument * * <pre><code> * { * common: { * desc: 'New description', * min: 0, * max: 100 * }, * native: { * unused: null * } * } * </code></pre> * * We will get as output: * <pre><code> * { * common: { * desc: 'New description', * min: 0, * max: 100 * }, * type: 'state', * native: { * } * } * </code></pre> * * * @alias extendObject * @memberof Adapter * @param {string} id object ID, that must be extended * @param {object} obj part that must be extended * @param {object} [options] optional user context * @param {ioBroker.ExtendObjectCallback} [callback] return result * <pre><code> * function (err, obj) { * if (err) adapter.log.error(err); * // obj is {"id": id} * } * </code></pre> */ this.extendObject = async (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!obj) { logger.error(`${this.namespaceLog} extendObject: try to extend null object for ${id}`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_EMPTY_OBJECT); } if (!tools.isObject(obj)) { logger.error(`${this.namespaceLog} extendObject: type of object parameter expected to be an object, but ${typeof obj} provided`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_NO_OBJECT); } if (!adapterObjects) { logger.info(`${this.namespaceLog} extendObject not processed because Objects database not connected`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { tools.validateGeneralObjectProperties(obj, true); } catch (e) { // todo: in the future we will not create this object logger.warn(`${this.namespaceLog} Object ${id} is invalid: ${e.message}`); logger.warn(`${this.namespaceLog} This object will not be created in future versions. Please report this to the developer.`); } try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } id = this._fixId(id, false/*, obj.type*/); const mId = id.replace(FORBIDDEN_CHARS, '_'); if (mId !== id) { logger.warn(`${this.namespaceLog} Used invalid characters: ${id} changed to ${mId}`); id = mId; } if (obj.children || obj.parent) { logger.warn(`${this.namespaceLog} Do not use parent or children for ${id}`); } // Read whole object let oldObj; try { oldObj = await adapterObjects.getObjectAsync(id, options); } catch (e) { return tools.maybeCallbackWithError(callback, e); } if (!adapterObjects) { logger.info(`${this.namespaceLog} extendObject not processed because Objects database not connected`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // remove the preserve attributes if (oldObj && options && tools.isObject(options.preserve)) { tools.removePreservedProperties(options.preserve, oldObj, obj); } // delete arrays if they should be changed if (obj && ( (obj.common && obj.common.members) || (obj.native && obj.native.repositories) || (obj.native && obj.native.certificates) || (obj.native && obj.native.devices)) ) { if (!oldObj) { logger.error(`${this.namespaceLog} Object ${id} not exist!`); oldObj = {}; } if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { oldObj.native.repositories = []; } if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { oldObj.common.members = []; } if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { oldObj.native.certificates = []; } if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { oldObj.native.devices = []; } obj.from = obj.from || (`system.adapter.${this.namespace}`); obj.user = obj.user || (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = obj.ts || Date.now(); obj = extend(true, oldObj, obj); return adapterObjects.setObject(id, obj, options, callback); } else { obj.from = obj.from || (`system.adapter.${this.namespace}`); obj.user = obj.user || (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = obj.ts || Date.now(); if ((obj.type && obj.type === 'state') || (!obj.type && oldObj && oldObj.type === 'state')) { if (obj.common && Object.prototype.hasOwnProperty.call(obj.common, 'custom') && obj.common.custom !== null && !tools.isObject(obj.common.custom)) { logger.error(this.namespaceLog + ' extendObject ' + id + ' (type=' + obj.type + ') property common.custom is of type ' + typeof obj.common.custom + ', expected object.'); return tools.maybeCallbackWithError(callback, 'common.custom needs to be an object'); } } else { if (obj.common && Object.prototype.hasOwnProperty.call(obj.common, 'custom') && obj.common.custom !== null) { logger.warn(this.namespaceLog + ' setObject ' + id + ' (type=' + obj.type + ') property common.custom must not exist.'); delete obj.common.custom; } } if (!oldObj) { // if old object is not existing we behave like setObject return this.setForeignObject(id, obj, options, callback); } try { const cbObj = await adapterObjects.extendObjectAsync(id, obj, options); let defState; if (obj.type === 'state' || oldObj.type === 'state') { if (obj.common && obj.common.def !== undefined) { defState = obj.common.def; } else if (oldObj.common && oldObj.common.def !== undefined) { defState = oldObj.common.def; } } if (defState !== undefined) { let currentStateObj; try { currentStateObj = await this.getForeignStateAsync(id); } catch { // do nothing } if (!currentStateObj) { try { await this.setForeignStateAsync(id, {val: defState, q: QUALITY_SUBS_INITIAL, ack: true}); } catch (e) { logger.info(`${this.namespaceLog} Default value for state "${id}" could not be set: ${e.message}`); } } } return tools.maybeCallbackWithError(callback, null, cbObj); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } }; /** * Promise-version of Adapter.extendObject */ this.extendObjectAsync = tools.promisify(this.extendObject, this); /** * Same as {@link Adapter.setObject}, but for any object. * * ID must be specified as a full name with adapter namespace. E.g "hm-rpc.0.ABC98989.1.STATE" * * @alias setForeignObject * @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 {ioBroker.SetObjectCallback} [callback] return result * <pre><code> * function (err, obj) { * // obj is {id: id} * if (err) adapter.log.error('Cannot write object: ' + err); * } * </code></pre> */ this.setForeignObject = (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!obj) { logger.error(`${this.namespaceLog} setForeignObject: try to set null object for ${id}`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_EMPTY_OBJECT); } if (!tools.isObject(obj)) { logger.error(`${this.namespaceLog} setForeignObject: type of object parameter expected to be an object, but ${typeof obj} provided`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_NO_OBJECT); } obj.from = obj.from || 'system.adapter.' + this.namespace; obj.user = obj.user || (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = obj.ts || Date.now(); if (id) { const mId = id.replace(FORBIDDEN_CHARS, '_'); if (mId !== id) { logger.warn(`${this.namespaceLog} Used invalid characters: ${id} changed to ${mId}`); id = mId; } } // check that alias is valid if given if (obj.common && obj.common.alias && obj.common.alias.id) { // if alias is object validate read and write if (typeof obj.common.alias.id === 'object') { try { validateId(obj.common.alias.id.write, true, null); validateId(obj.common.alias.id.read, true, null); } catch (e) { return tools.maybeCallbackWithError(callback, `Alias id is invalid: ${e.message}`); } if (obj.common.alias.id.write.startsWith(ALIAS_STARTS_WITH) || obj.common.alias.id.read.startsWith(ALIAS_STARTS_WITH)) { return tools.maybeCallbackWithError(callback, 'Aliases cannot be used as target for aliases'); } } else { try { validateId(obj.common.alias.id, true, null); } catch (e) { return tools.maybeCallbackWithError(callback, `Alias id is invalid: ${e.message}`); } if (obj.common.alias.id.startsWith(ALIAS_STARTS_WITH)) { return tools.maybeCallbackWithError(callback, 'Aliases cannot be used as target for aliases'); } } } setObjectWithDefaultValue(id, obj, options, callback); }; /** * Promise-version of Adapter.setForeignObject */ this.setForeignObjectAsync = tools.promisify(this.setForeignObject, this); /** * Same as {@link Adapter.extendObject}, but for any object. * * ID must be specified as a full name with adapter namespace. E.g "hm-rpc.0.ABC98989.1.STATE" * * @alias extendForeignObject * @memberof Adapter * @param {string} id object ID, that must be extended * @param {object} obj part that must be extended * @param {object} [options] optional user context, or use attribute preserve e.g. {preserve: {common: ['name']}} to preserve common.name * @param {ioBroker.SetObjectCallback} [callback] return result * <pre><code> * function (err, obj) { * // obj is {"id": id} * if (err) adapter.log.error(err); * } * </code></pre> */ this.extendForeignObject = async (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { logger.info(`${this.namespaceLog} extendForeignObject not processed because Objects database not connected`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } const mId = id.replace(FORBIDDEN_CHARS, '_'); if (mId !== id) { logger.warn(`${this.namespaceLog} Used invalid characters: ${id} changed to ${mId}`); id = mId; } if (!obj) { logger.error(`${this.namespaceLog} extendForeignObject: try to set null object for ${id}`); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_EMPTY_OBJECT); } // Read whole object let oldObj; try { oldObj = await adapterObjects.getObjectAsync(id, options); } catch (e) { return tools.maybeCallbackWithError(callback, e); } // remove the preserve attributes if (oldObj && options && tools.isObject(options.preserve)) { tools.removePreservedProperties(options.preserve, oldObj, obj); } // delete arrays if they should be changed if (obj && ( (obj.common && obj.common.members) || (obj.native && obj.native.repositories) || (obj.native && obj.native.certificates) || (obj.native && obj.native.devices)) ) { if (!oldObj) { logger.error(`${this.namespaceLog} Object ${id} not exist!`); oldObj = {}; } if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { oldObj.native.repositories = []; } if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { oldObj.common.members = []; } if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { oldObj.native.certificates = []; } if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { oldObj.native.devices = []; } obj.from = obj.from || (`system.adapter.${this.namespace}`); obj.user = obj.user || (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = obj.ts || Date.now(); obj = extend(true, oldObj, obj); return adapterObjects.setObject(id, obj, options, callback); } else { obj.from = obj.from || (`system.adapter.${this.namespace}`); obj.user = obj.user || (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = obj.ts || Date.now(); if ((obj.type && obj.type === 'state') || (!obj.type && oldObj && oldObj.type === 'state')) { if (obj.common && Object.prototype.hasOwnProperty.call(obj.common, 'custom') && obj.common.custom !== null && !tools.isObject(obj.common.custom)) { logger.error(this.namespaceLog + ' extendObject ' + id + ' (type=' + obj.type + ') property common.custom is of type ' + typeof obj.common.custom + ', expected object.'); return tools.maybeCallbackWithError(callback, 'common.custom needs to be an object'); } } else { if (obj.common && Object.prototype.hasOwnProperty.call(obj.common, 'custom') && obj.common.custom !== null) { logger.warn(this.namespaceLog + ' setObject ' + id + ' (type=' + obj.type + ') property common.custom must not exist.'); delete obj.common.custom; } } if (!oldObj) { // if old object is not existing we behave like setObject return this.setForeignObject(id, obj, options, callback); } try { const cbObj = await adapterObjects.extendObjectAsync(id, obj, options); if (cbObj.value.type === 'state') { let defState; if (obj.common && obj.common.def !== undefined) { defState = obj.common.def; } else if (oldObj.common && oldObj.common.def !== undefined) { defState = oldObj.common.def; } if (defState !== undefined) { let currentStateObj; try { currentStateObj = await this.getForeignStateAsync(id); } catch { // do nothing } if (!currentStateObj) { try { await this.setForeignStateAsync(id, {val: defState, q: QUALITY_SUBS_INITIAL, ack: true}); } catch (e) { logger.info(`${this.namespaceLog} Default value for state "${id}" could not be set: ${e.message}`); } } } } return tools.maybeCallbackWithError(callback, null, cbObj); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } }; /** * Promise-version of Adapter.extendForeignObject */ this.extendForeignObjectAsync = tools.promisify(this.extendForeignObject, this); /** * Get object of this instance. * * It is not required, that ID consists namespace. E.g. to get object of "adapterName.X.myObject", only "myObject" is required as ID. * * @alias getObject * @memberof Adapter * @param {string} id exactly object ID (without namespace) * @param {object} [options] optional user context * @param {ioBroker.GetObjectCallback} callback return result * <pre><code> * function (err, obj) { * if (err) adapter.log.error('Cannot get object: ' + err); * } * </code></pre> */ this.getObject = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('getObject not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } adapterObjects.getObject(this._fixId(id), options, callback); }; /** * Promise-version of Adapter.getObject */ this.getObjectAsync = tools.promisify(this.getObject, this); /** * Read object view from DB. * * It is required, that ID consists namespace in startkey and endkey. E.g. {startkey: 'hm-rpc.' + adapter.instance + '.', endkey: 'hm-rpc.' + adapter.instance + '.\u9999'} * to get all objects of the instance. * * @alias getObjectView * @memberof Adapter * @param {string} design name of the design * @param {string} search name of the view * @param {object} params object containing startkey: first id to include in result; endkey: last id to include in result * @param {object} options * @param {ioBroker.GetObjectViewCallback} callback return result * <pre><code> * function (err, doc) { * if (doc && doc.rows) { * for (var i = 0; i < doc.rows.length; i++) { * var id = doc.rows[i].id; * var obj = doc.rows[i].value; * console.log('Found ' + id + ': ' + JSON.stringify(obj)); * } * if (!doc.rows.length) console.log('No objects found.'); * } else { * console.log('No objects found: ' + err); * } * } * </code></pre> */ this.getObjectView = (design, search, params, options, callback) => { if (typeof options === 'function') { callback = options; options = undefined; } if (!adapterObjects) { this.log.info('getObjectView not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } return adapterObjects.getObjectView(design, search, params, options, callback); }; /** * Promise-version of Adapter.getObjectView */ this.getObjectViewAsync = tools.promisify(this.getObjectView, this); // TODO: remove this backward compatibility shim in the future this.objects.getObjectView = (design, search, params, options, callback) => { this.log.warn('adapter.objects.getObjectView is deprecated, and will be removed in the future. Please use adapter.getObjectView/Async. Report this to Developer!'); return this.getObjectView(design, search, params, options, callback); }; /** * Read object list from DB. * * It is required, that ID consists namespace in startkey and endkey. E.g. {startkey: 'hm-rpc.' + adapter.instance + '.', endkey: 'hm-rpc.' + adapter.instance + '.\u9999'} * to get all objects of the instance. * * @alias getObjectList * @memberof Adapter * * @param {object} params * @param {object} options * @param {ioBroker.GetObjectListCallback} callback * <pre><code> * function (err, res) { * if (res && res.rows) { * for (var i = 0; i < res.rows.length; i++) { * var id = res.rows[i].id; * var obj = res.rows[i].value; * console.log('Found ' + id + ': ' + JSON.stringify(obj)); * } * if (!res.rows.length) console.log('No objects found.'); * } else { * console.log('No objects found: ' + err); * } * } * </code></pre> */ this.getObjectList = (params, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('getObjectList not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.getObjectList(params, options, callback); }; /** * Promise-version of Adapter.getObjectList */ this.getObjectListAsync = tools.promisify(this.getObjectList, this); // TODO: remove this backward compatibility shim in the future this.objects.getObjectList = (params, options, callback) => { this.log.warn('adapter.objects.getObjectList is deprecated, and will be removed in the future. Please use adapter.getObjectList/Async. Report this to Developer!'); adapterObjects.getObjectList(params, options, callback); }; /** * Get the enum tree. * * Get enums of specified tree or all enums if nothing specified as object with values. * If getEnum called with no enum specified, all enums will be returned: * <pre><code> * adapter.getEnums(function (err, enums, requestEnum) { * // All enums * if (err) adapter.log.error('Cannot get object: ' + err); * for (var e in enums) { * adapter.log.debug('Enum "' + e + '" has following members: ' + enums[e].common.members.join(', ')); * } * }); * </code></pre> * * @alias getEnum * @memberof Adapter * @param {string} _enum enum name, e.g. 'rooms', 'function' or '' (all enums) * @param {object} [options] optional user context * @param {ioBroker.GetEnumCallback} callback return result * <pre><code> * function (err, enums, requestEnum) { * // requestEnum is _enum * if (err) adapter.log.error('Cannot get object: ' + err); * for (var e in enums) { * adapter.log.debug('Enum "' + e + '" has following members: ' + enums[e].common.members.join(', ')); * } * } * </code></pre> */ this.getEnum = (_enum, options, callback) => { if (typeof _enum === 'function') { callback = _enum; options = null; _enum = ''; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('getEnum not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!_enum.match('^enum.')) { _enum = 'enum.' + _enum; } const result = {}; adapterObjects.getObjectView('system', 'enum', {startkey: _enum + '.', endkey: _enum + '.\u9999'}, options, (err, res) => { if (err) { return tools.maybeCallbackWithError(callback, err); } if (res && res.rows) { for (let t = 0; t < res.rows.length; t++) { result[res.rows[t].id] = res.rows[t].value; } } return tools.maybeCallbackWithError(callback, err, result, _enum); }); }; /** * Promise-version of Adapter.getEnum */ this.getEnumAsync = tools.promisify(this.getEnum, this, ['result', 'requestEnum']); /** * Read the members of given enums. * * Get enums of specified tree or all enums if nothing specified as object with values. * * @alias getEnums * @memberof Adapter * @param {string|array} _enumList enum name or names, e.g. ['rooms', 'function'] * @param {object} [options] optional user context * @param {ioBroker.GetEnumsCallback} callback return result * <pre><code> * function (err, enums) { * // requestEnum is _enum * if (err) adapter.log.error('Cannot get object: ' + err); * // Result is like * // { * // "enum.rooms": { * // "enum.rooms.livingroom": { * // common: { * // members: ['ID1', 'ID2'] * // } * // }, * // "enum.rooms.sleepingroom": { * // common: { * // members: ['ID3', 'ID4'] * // } * // } * // }, * // "enum.functions": { * // "enum.rooms.light": { * // common: { * // members: ['ID1', 'ID6'] * // } * // }, * // "enum.rooms.weather": { * // common: { * // members: ['ID4', 'ID7'] * // } * // } * // } * // } * } * </code></pre> */ this.getEnums = (_enumList, options, callback) => { if (typeof _enumList === 'function') { callback = _enumList; _enumList = null; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('getEnums not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } const _enums = {}; if (_enumList) { if (typeof _enumList === 'string') { _enumList = [_enumList]; } const promises = []; for (const currEnum of _enumList) { promises.push(new Promise((resolve, reject) => { this.getEnum(currEnum, options, (err, list, _enum) => { if (err) { return reject(err); } else if (list) { _enums[_enum] = list; } resolve(); }); })); } Promise.all(promises) .then(() => { return tools.maybeCallbackWithError(callback, null, _enums); }) .catch(e => { return tools.maybeCallbackWithError(callback, e); }); } else { // Read all enums adapterObjects.getObjectView('system', 'enum', {startkey: 'enum.', endkey: 'enum.\u9999'}, options, (err, res) => { // be aware, that res.rows[x].id is the name of enum! if (err) { return tools.maybeCallbackWithError(callback, err); } const result = {}; if (res && res.rows) { for (let i = 0; i < res.rows.length; i++) { const parts = res.rows[i].id.split('.', 3); if (!parts[2]) { continue; } if (!result[parts[0] + '.' + parts[1]]) { result[parts[0] + '.' + parts[1]] = {}; } result[parts[0] + '.' + parts[1]][res.rows[i].id] = res.rows[i].value; } } return tools.maybeCallbackWithError(callback, err, result); }); } }; /** * Promise-version of Adapter.getEnums */ this.getEnumsAsync = tools.promisify(this.getEnums, this); /** * Get objects by pattern, by specific type and resolve their enums. * * Get all objects in the system of specified type. E.g.: * * <pre><code> * adapter.getForeignObjects('hm-rega.0.*', 'state', ['rooms', 'functions'], function (err, objs) { * if (err) adapter.log.error('Cannot get object: ' + err); * // objs look like: * // { * // "hm-rega.0.ABC0000.1.STATE": { * // common: {...}, * // native: {}, * // type: 'state', * // enums: { * // 'enums.rooms.livingroom': 'Living room', * // 'enums.functions.light': 'Light' * // } * // }, * // "hm-rega.0.ABC0000.2.STATE": { * // common: {...}, * // native: {}, * // type: 'state', * // enums: { * // 'enums.rooms.sleepingroom': 'Sleeping room', * // 'enums.functions.window': 'Windows' * // } * // } * } * </code></pre> * * @alias getForeignObjects * @memberof Adapter * @param {string} pattern object ID/wildchars * @param {string} type type of object: 'state', 'channel' or 'device'. Default - 'state' * @param {string|string[]} enums object ID, that must be overwritten or created. * @param {object} [options] optional user context * @param {ioBroker.GetObjectsCallback} callback return result * <pre><code> * function (err, obj) { * if (err) adapter.log.error('Cannot get object: ' + err); * } * </code></pre> */ this.getForeignObjects = (pattern, type, enums, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } let params = {}; if (pattern && pattern !== '*') { params = { startkey: pattern.replace(/\*/g, ''), endkey: pattern.replace(/\*/g, '\u9999') }; } if (typeof enums === 'function') { callback = enums; enums = null; } if (typeof type === 'function') { callback = type; type = null; } if (typeof type === 'object') { options = type; type = null; } if (typeof enums === 'object' && !Array.isArray(enums)) { options = enums; enums = null; } if (!adapterObjects) { this.log.info('getForeignObjects not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.getObjectView('system', type || 'state', params, options, (err, res) => { if (err) { return tools.maybeCallbackWithError(callback, err); } // don't forget, that enums returns names in row[x].id and not IDs, you can find id in rows[x].value._id this.getEnums(enums, null, (err, _enums) => { const list = {}; if (res && res.rows) { for (let i = 0; i < res.rows.length; i++) { if (!res.rows[i].value) { logger.debug(`${this.namespaceLog} getEnums(${JSON.stringify(enums)}) returned an enum without a value at index ${i}, obj - ${JSON.stringify(res.rows[i])}`); continue; } const id = res.rows[i].value._id; list[id] = res.rows[i].value; if (_enums && id) { // get device or channel of this state and check it too const parts = id.split('.'); parts.splice(parts.length - 1, 1); const channel = parts.join('.'); parts.splice(parts.length - 1, 1); const device = parts.join('.'); list[id].enums = {}; for (const es in _enums) { if (!Object.prototype.hasOwnProperty.call(_enums, es)) { continue; } for (const e in _enums[es]) { if (!Object.prototype.hasOwnProperty.call(_enums[es], e)) { continue; } if (!_enums[es][e] || !_enums[es][e].common || !_enums[es][e].common.members) { continue; } if (_enums[es][e].common.members.indexOf(id) !== -1 || _enums[es][e].common.members.indexOf(channel) !== -1 || _enums[es][e].common.members.indexOf(device) !== -1) { list[id].enums[e] = _enums[es][e].common.name; } } } } } } return tools.maybeCallbackWithError(callback, null, list); }); }); }; /** * Promise-version of Adapter.getForeignObjects */ this.getForeignObjectsAsync = tools.promisify(this.getForeignObjects, this); /** * Find any object by name or ID. * * Find object by the exact name or ID. * * @alias findForeignObject * @memberof Adapter * @param {string} id exactly object ID (without namespace) * @param {string} type optional common.type of state: 'number', 'string', 'boolean', 'file', ... * @param {object} options optional user context * @param {ioBroker.FindObjectCallback} callback return result * <pre><code> * adapter.findForeignObject('Some name', function (err, id, name) { * if (err) adapter.log.error('Cannot get object: ' + err); * adapter.log.debug('ID of object with name "' + name + '" is "' + id + '"'); * } * </code></pre> */ this.findForeignObject = (id, type, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (typeof type === 'function') { callback = type; type = null; } if (!adapterObjects) { this.log.info('findForeignObject not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } adapterObjects.findObject(id, type, options, callback); }; /** * Promise-version of Adapter.findForeignObject */ this.findForeignObjectAsync = tools.promisify(this.findForeignObject, this, ['id', 'name']); /** * Get any object. * * ID must be specified with namespace. * * @alias getForeignObject * @memberof Adapter * @param {string} id exactly object ID (with namespace) * @param {object} [options] optional user context * @param {ioBroker.GetObjectCallback} callback return result * <pre><code> * function (err, obj) { * if (err) adapter.log.error('Cannot get object: ' + err); * } * </code></pre> */ this.getForeignObject = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('getForeignObject not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } adapterObjects.getObject(id, options, (err, obj) => { const adapterName = this.namespace.split('.')[0]; // remove protectedNative if not admin or own adapter if (obj && obj.protectedNative && obj.protectedNative.length && obj._id && obj._id.startsWith('system.adapter.') && adapterName !== 'admin' && adapterName !== obj._id.split('.')[2] ) { for (const attr of obj.protectedNative) { delete obj.native[attr]; } // endFor } // endIf return tools.maybeCallbackWithError(callback, err, obj); }); }; /** * Promise-version of Adapter.getForeignObject */ this.getForeignObjectAsync = tools.promisify(this.getForeignObject, this); /** * Delete an object of this instance. * * It is not required to provice the adapter namespace, because it will automatically be added. * E.g. to delete "adapterName.X.myObject", only "myObject" is required as ID. * * The corresponding state will be deleted too if the object has type "state". * * @alias delObject * @memberof Adapter * @param {string} id exactly object ID (without namespace) * @param {object} [options] optional user context. E.g. recursive option could be true * @param {ioBroker.ErrorCallback} [callback] return result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot delete object: ' + err); * } * </code></pre> */ this.delObject = (id, options, callback) => { try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } // delObject does the same as delForeignObject, but fixes the ID first id = this._fixId(id); this.delForeignObject(id, options, callback); }; /** * Promise-version of Adapter.delObject */ this.delObjectAsync = tools.promisify(this.delObject, this); const _deleteObjects = (tasks, options, cb) => { if (!tasks || !tasks.length) { return tools.maybeCallback(cb); } else { const task = tasks.shift(); adapterObjects.delObject(task.id, options, err => { if (err) { return tools.maybeCallbackWithError(cb, err); } else if (!task.state) { // if not a state, we dont have to delete state tools.removeIdFromAllEnums(adapterObjects, task.id).then(() => setImmediate(_deleteObjects, tasks, options, cb)) .catch(e => { this.log.warn(`Could not remove ${task.id} from enums: ${e.message}`); setImmediate(_deleteObjects, tasks, options, cb); }); } else { this.delForeignState(task.id, options, err => { err && this.log.warn(`Could not remove state of ${task.id}`); tools.removeIdFromAllEnums(adapterObjects, task.id).then(() => setImmediate(_deleteObjects, tasks, options, cb)) .catch(e => { this.log.warn(`Could not remove ${task.id} from enums: ${e.message}`); setImmediate(_deleteObjects, tasks, options, cb); }); }); } }); } }; /** * Delete any object. * * The full ID with namespace must be specified. The corresponding state will be deleted too if the object has type "state". * * @alias delForeignObject * @memberof Adapter * @param {string} id exactly object ID (with namespace) * @param {object} [options] optional user context or {recursive:true} to delete all underlying objects * @param {ioBroker.ErrorCallback} [callback] return result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot delete object: ' + err); * } * </code></pre> */ this.delForeignObject = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('delForeignObject not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } // If recursive deletion of all underlying objects, including id if (options && options.recursive) { // read object itself adapterObjects.getObject(id, options, (err, obj) => { const tasks = obj && (!obj.common || !obj.common.dontDelete) ? [{id, state: obj.type === 'state'}] : []; const selector = {startkey: id + '.', endkey: id + '.\u9999'}; // read all underlying states adapterObjects.getObjectList(selector, options, (err, res) => { res && res.rows && res.rows.forEach(item => !tasks.find(task => task.id === item.id) && (!item.value || !item.value.common || !item.value.common.dontDelete) && // exclude objects with dontDelete flag tasks.push({id: item.id, state: item.value && item.value.type === 'state'})); _deleteObjects(tasks, options, callback); }); }); } else { adapterObjects.getObject(id, options, (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else if (!obj) { // obj non existing we can return right now return tools.maybeCallback(callback); } else { // do not allow deletion of objects with dontDelete flag if (!obj.common || !obj.common.dontDelete) { adapterObjects.delObject(obj._id, options, err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else if (obj.type !== 'state') { tools.removeIdFromAllEnums(adapterObjects, id) .then(() => { return tools.maybeCallback(callback); }) .catch(e => { return tools.maybeCallbackWithError(callback, e); }); } else { if (obj.binary) { this.delBinaryState(id, options, () => tools.removeIdFromAllEnums(adapterObjects, id) .then(() => { return tools.maybeCallback(callback); }) .catch(e => { return tools.maybeCallbackWithError(callback, e); })); } else { this.delForeignState(id, options, () => tools.removeIdFromAllEnums(adapterObjects, id) .then(() => { return tools.maybeCallback(callback); }) .catch(e => { return tools.maybeCallbackWithError(callback, e); })); } } }); } else { return tools.maybeCallbackWithError(callback, 'not deletable'); } } }); } }; /** * Promise-version of Adapter.delForeignObject */ this.delForeignObjectAsync = tools.promisify(this.delForeignObject, this); /** * Subscribe for the changes of objects in this instance. * * @alias subscribeObjects * @memberof Adapter * @param {string} pattern pattern like 'channel.*' or '*' (all objects of this adapter) - without namespaces * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] optional returns result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot subscribe object: ' + err); * } * </code></pre> */ this.subscribeObjects = (pattern, options, callback) => { if (typeof options === 'function') { callback = options; options = undefined; } if (!adapterObjects) { this.log.info('subscribeObjects not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (pattern === '*') { adapterObjects.subscribeUser(this.namespace + '.*', options, callback); } else { pattern = this._fixId(pattern, true); adapterObjects.subscribeUser(pattern, options, callback); } }; /** * Promise-version of Adapter.subscribeObjects */ this.subscribeObjectsAsync = tools.promisify(this.subscribeObjects, this); /** * Unsubscribe on the changes of objects in this instance. * * @alias unsubscribeObjects * @memberof Adapter * @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] optional returns result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot unsubscribe object: ' + err); * } * </code></pre> */ this.unsubscribeObjects = (pattern, options, callback) => { if (typeof options === 'function') { callback = options; options = undefined; } if (!adapterObjects) { this.log.info('unsubscribeObjects not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (pattern === '*') { adapterObjects.unsubscribeUser(this.namespace + '.*', options, callback); } else { pattern = this._fixId(pattern, true); adapterObjects.unsubscribeUser(pattern, options, callback); } }; /** * Promise-version of Adapter.unsubscribeObjects */ this.unsubscribeObjectsAsync = tools.promisify(this.unsubscribeObjects, this); /** * Subscribe for the changes of objects in any instance. * * @alias subscribeForeignObjects * @memberof Adapter * @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces. You can use array of patterns * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] optional returns result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot subscribe object: ' + err); * } * </code></pre> */ this.subscribeForeignObjects = (pattern, options, callback) => { if (typeof options === 'function') { callback = options; options = undefined; } if (!adapterObjects) { this.log.info('subscribeForeignObjects not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.subscribeUser(pattern, options, callback); }; /** * Promise-version of Adapter.subscribeForeignObjects */ this.subscribeForeignObjectsAsync = tools.promisify(this.subscribeForeignObjects, this); /** * Unsubscribe for the patterns on all objects. * * @alias unsubscribeForeignObjects * @memberof Adapter * @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] optional returns result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot unsubscribe object: ' + err); * } * </code></pre> */ this.unsubscribeForeignObjects = (pattern, options, callback) => { if (typeof options === 'function') { callback = options; options = undefined; } if (!pattern) { pattern = '*'; } if (!adapterObjects) { this.log.info('unsubscribeForeignObjects not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.unsubscribeUser(pattern, options, callback); }; /** * Promise-version of Adapter.unsubscribeForeignObjects */ this.unsubscribeForeignObjectsAsync = tools.promisify(this.unsubscribeForeignObjects, this); /** * Same as {@link Adapter.setObject}, but with check if the object exists. * * ID must be specified as a full name with adapter namespace. E.g "hm-rpc.0.ABC98989.1.STATE". * New object will be created only if no object exists with such ID. * * @alias setObjectNotExists * @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 {ioBroker.SetObjectCallback} [callback] return result * <pre><code> * function (err, obj) { * // obj is {id: id} * if (err) adapter.log.error('Cannot write object: ' + err); * } * </code></pre> * @returns {Promise<{id: string}>} */ this.setObjectNotExists = async (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('setObjectNotExists not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } id = this._fixId(id); if (obj.children || obj.parent) { logger.warn(`${this.namespaceLog} Do not use parent or children for ${id}`); } // check if object already exists let objExists; try { objExists = await adapterObjects.objectExists(id, options); } catch (e) { return tools.maybeCallbackWithError(callback, `Could not check object existence of ${id}: ${e.message}`); } if (objExists === false) { if (!obj.from) { obj.from = 'system.adapter.' + this.namespace; } if (!obj.user) { obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; } if (!obj.ts) { obj.ts = Date.now(); } return setObjectWithDefaultValue(id, obj, null, callback); } else { return tools.maybeCallbackWithError(callback, null); } }; /** * Promise-version of Adapter.setObjectNotExists */ this.setObjectNotExistsAsync = tools.promisify(this.setObjectNotExists, this); /** * Same as {@link Adapter.setForeignObject}, but with check if the object exists. * * ID must be specified as a full name with adapter namespace. E.g "hm-rpc.0.ABC98989.1.STATE". * New object will be created only if no object exists with such ID. * * @alias setForeignObjectNotExists * @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 {ioBroker.SetObjectCallback} [callback] return result * <pre><code> * function (err, obj) { * // obj is {id: id} * if (err) adapter.log.error('Cannot write object: ' + err); * } * </code></pre> * @returns {Promise<{id: string}>} */ this.setForeignObjectNotExists = async (id, obj, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('setForeignObjectNotExists not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } // check if object exists let objExists; try { objExists = await adapterObjects.objectExists(id, options); } catch (e) { return tools.maybeCallbackWithError(callback, `Could not check object existence of ${id}: ${e.message}`); } if (objExists === false) { if (!obj.from) { obj.from = 'system.adapter.' + this.namespace; } if (!obj.user) { obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; } if (!obj.ts) { obj.ts = Date.now(); } return setObjectWithDefaultValue(id, obj, null, callback); } else { return tools.maybeCallbackWithError(callback, null); } }; /** * Promise-version of Adapter.setForeignObjectNotExists */ this.setForeignObjectNotExistsAsync = tools.promisify(this.setForeignObjectNotExists, this); this._DCS2ID = (device, channel, stateOrPoint) => { let id = ''; if (device) { id += device; } if (channel) { id += (id ? '.' : '') + channel; } if (stateOrPoint !== true && stateOrPoint !== false) { if (stateOrPoint) { id += (id ? '.' : '') + stateOrPoint; } } else if (stateOrPoint === true && id) { id += '.'; } return id; }; this.createDevice = (deviceName, common, _native, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!deviceName) { logger.error(this.namespaceLog + ' Try to create device with empty name!'); return; } if (typeof _native === 'function') { callback = _native; _native = {}; } if (typeof common === 'function') { callback = common; common = {}; } common = common || {}; common.name = common.name || deviceName; deviceName = deviceName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); _native = _native || {}; this.setObjectNotExists(deviceName, { type: 'device', common: common, native: _native }, options, callback); }; /** * Promise-version of Adapter.createDevice */ this.createDeviceAsync = tools.promisify(this.createDevice, this); // name of channel must be in format "channel" this.createChannel = (parentDevice, channelName, roleOrCommon, _native, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!channelName) { throw new Error('Cannot create a channel without a name!'); } if (typeof _native === 'function') { callback = _native; _native = {}; } if (typeof roleOrCommon === 'function') { callback = roleOrCommon; roleOrCommon = undefined; } let common = {}; if (typeof roleOrCommon === 'string') { common = { name: '', role: roleOrCommon }; } else if (typeof roleOrCommon === 'object') { common = roleOrCommon; } common.name = common.name || channelName; if (parentDevice) { parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } channelName = channelName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); channelName = this._DCS2ID(parentDevice, channelName); _native = _native || {}; const obj = { type: 'channel', common: common, native: _native }; this.setObjectNotExists(channelName, obj, options, callback); }; /** * Promise-version of Adapter.createChannel */ this.createChannelAsync = tools.promisify(this.createChannel, this); this.createState = (parentDevice, parentChannel, stateName, roleOrCommon, _native, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!stateName) { throw new Error('Cannot create a state without a name!'); } if (typeof _native === 'function') { callback = _native; _native = {}; } if (typeof roleOrCommon === 'function') { callback = roleOrCommon; roleOrCommon = undefined; } /** @type {ioBroker.StateCommon} */ let common = {}; if (typeof roleOrCommon === 'string') { common = { read: true, write: false, name: '', role: roleOrCommon }; } else if (typeof roleOrCommon === 'object') { common = roleOrCommon; } common.name = common.name || stateName; _native = _native || {}; common.read = (common.read === undefined) ? true : common.read; common.write = (common.write === undefined) ? false : common.write; if (!common.role) { logger.error(this.namespaceLog + ' Try to create state ' + (parentDevice ? (parentDevice + '.') : '') + parentChannel + '.' + stateName + ' without role'); return; } if (parentDevice) { parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (parentChannel) { parentChannel = parentChannel.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } stateName = stateName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const id = this._fixId({device: parentDevice, channel: parentChannel, state: stateName}); // Check min, max and def values for number if (common.type !== undefined && common.type === 'number') { let min = 0; let max = 0; let def = 0; let err; if (common.min !== undefined) { min = common.min; if (typeof min !== 'number') { min = parseFloat(min); if (isNaN(min)) { err = 'Wrong type of ' + id + '.common.min'; logger.error(this.namespaceLog + ' ' + err); return tools.maybeCallbackWithError(callback, err); } else { common.min = min; } } } if (common.max !== undefined) { max = common.max; if (typeof max !== 'number') { max = parseFloat(max); if (isNaN(max)) { err = 'Wrong type of ' + id + '.common.max'; logger.error(this.namespaceLog + ' ' + err); return tools.maybeCallbackWithError(callback, err); } else { common.max = max; } } } if (common.def !== undefined) { def = common.def; if (typeof def !== 'number') { def = parseFloat(def); if (isNaN(def)) { err = new Error('Wrong type of ' + id + '.common.def'); logger.error(`${this.namespaceLog} ${err.message}`); return tools.maybeCallbackWithError(callback, err); } else { common.def = def; } } } if (common.min !== undefined && common.max !== undefined && min > max) { common.max = min; common.min = max; } if (common.def !== undefined && common.min !== undefined && def < min) { common.def = min; } if (common.def !== undefined && common.max !== undefined && def > max) { common.def = max; } } this.setObjectNotExists(id, { type: 'state', common: common, native: _native }, options, err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else if (common.def !== undefined) { this.getState(id, null, (err, state) => { if (!state) { if (common.defAck !== undefined) { this.setState(id, common.def, common.defAck, options, callback); } else { this.setState(id, common.def, options, callback); } } else { return tools.maybeCallback(callback); } }); } else { this.getState(id, null, (err, state) => { if (!state) { this.setState(id, null, true, options, callback); } else { return tools.maybeCallback(callback); } }); } }); }; /** * Promise-version of Adapter.createState */ this.createStateAsync = tools.promisify(this.createState, this); /** * Delete device with all its channels and states. * * @alias deleteDevice * @memberof Adapter * @param {string} deviceName is the part of ID like: adapter.instance.<deviceName> * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] return result * <pre><code> * function (err, obj) { * // obj is {id: id} * if (err) adapter.log.error('Cannot write object: ' + err); * } * </code></pre> */ this.deleteDevice = (deviceName, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('deleteDevice not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } deviceName = deviceName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); if (!this._namespaceRegExp.test(deviceName)) { deviceName = this.namespace + '.' + deviceName; } adapterObjects.getObjectView('system', 'device', {startkey: deviceName, endkey: deviceName}, options, (err, res) => { if (err || !res || !res.rows) { return tools.maybeCallbackWithError(callback, err); } let cnt = 0; if (res.rows.length > 1) { logger.warn(this.namespaceLog + ' Found more than one device ' + deviceName); } for (let t = 0; t < res.rows.length; t++) { cnt++; this.delObject(res.rows[t].id, options, err => { if (err) { return tools.maybeCallbackWithError(callback, err); } if (!--cnt) { let _cnt = 0; _cnt++; // read channels of device adapterObjects.getObjectView('system', 'channel', {startkey: deviceName + '.', endkey: deviceName + '.\u9999'}, options, (err, res) => { _cnt--; if (err || !res || !res.rows) { return tools.maybeCallbackWithError(callback, err); } for (let k = 0; k < res.rows.length; k++) { _cnt++; this.deleteChannel(deviceName, res.rows[k].id, options, err => { if (!--_cnt) { typeof callback === 'function' && callback(err); callback = null; } else { if (err) { typeof callback === 'function' && callback(err); callback = null; } } }); } if (!_cnt && typeof callback === 'function') { callback(); callback = null; } }); // read states of the device... _cnt++; adapterObjects.getObjectView('system', 'state', {startkey: deviceName + '.', endkey: deviceName + '.\u9999'}, options, (err, res) => { _cnt--; if (err || !res || !res.rows) { return tools.maybeCallbackWithError(callback, err); } for (let k = 0; k < res.rows.length; k++) { _cnt++; this.deleteState(deviceName, '', res.rows[k].id, options, err => { if (!--_cnt) { typeof callback === 'function' && callback(err); callback = null; } else { if (err) { typeof callback === 'function' && callback(err); callback = null; } } }); } if (!_cnt && typeof callback === 'function') { callback(); callback = null; } }); } }); } if (!cnt && typeof callback === 'function') { callback(); callback = null; } }); }; /** * Promise-version of Adapter.deleteDevice */ this.deleteDeviceAsync = tools.promisify(this.deleteDevice, this); this.addChannelToEnum = (enumName, addTo, parentDevice, channelName, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('addChannelToEnum not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (parentDevice) { if (this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (this._namespaceRegExp.test(channelName)) { channelName = channelName.substring(this.namespace.length + 1); } if (parentDevice && channelName.substring(0, parentDevice.length) === parentDevice) { channelName = channelName.substring(parentDevice.length + 1); } channelName = channelName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const objId = this.namespace + '.' + this._DCS2ID(parentDevice, channelName); if (addTo.match(/^enum\./)) { adapterObjects.getObject(addTo, options, (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else if (obj) { const pos = obj.common.members.indexOf(objId); if (pos === -1) { obj.common.members.push(objId); obj.from = 'system.adapter.' + this.namespace; obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = Date.now(); adapterObjects.setObject(obj._id, obj, options, callback); } else { return tools.maybeCallback(callback); } } }); } else { if (enumName.match(/^enum\./)) { enumName = enumName.substring(5); } adapterObjects.getObject('enum.' + enumName + '.' + addTo, options, (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } if (obj) { const pos = obj.common.members.indexOf(objId); if (pos === -1) { obj.common.members.push(objId); obj.from = 'system.adapter.' + this.namespace; obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = Date.now(); adapterObjects.setObject(obj._id, obj, options, callback); } else { return tools.maybeCallback(callback); } } else { // Create enum adapterObjects.setObject('enum.' + enumName + '.' + addTo, { common: { name: addTo, members: [objId] }, from: 'system.adapter.' + this.namespace, ts: Date.now(), type: 'enum' }, options, callback); } }); } }; /** * Promise-version of Adapter.addChannelToEnum */ this.addChannelToEnumAsync = tools.promisify(this.addChannelToEnum, this); this.deleteChannelFromEnum = (enumName, parentDevice, channelName, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('deleteChannelFromEnum not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (parentDevice) { if (parentDevice.substring(0, this.namespace.length) === this.namespace) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (channelName && channelName.substring(0, this.namespace.length) === this.namespace) { channelName = channelName.substring(this.namespace.length + 1); } if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { channelName = channelName.substring(parentDevice.length + 1); } channelName = channelName || ''; channelName = channelName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const objId = this.namespace + '.' + this._DCS2ID(parentDevice, channelName); if (enumName) { enumName = 'enum.' + enumName + '.'; } else { enumName = 'enum.'; } adapterObjects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, async (err, res) => { if (err) { return tools.maybeCallbackWithError(callback, err); } if (res && res.rows) { for (let i = 0; i < res.rows.length; i++) { try { const obj = await adapterObjects.getObject(res.rows[i].id, options); if (obj && obj.common && obj.common.members) { const pos = obj.common.members.indexOf(objId); if (pos !== -1) { obj.common.members.splice(pos, 1); obj.from = 'system.adapter.' + this.namespace; obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = Date.now(); await adapterObjects.setObjectAsync(obj._id, obj, options); } } } catch (e) { return tools.maybeCallbackWithError(callback, e); } } } return tools.maybeCallback(callback); }); }; /** * Promise-version of Adapter.deleteChannelFromEnum */ this.deleteChannelFromEnumAsync = tools.promisify(this.deleteChannelFromEnum, this); this.deleteChannel = (parentDevice, channelName, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (typeof channelName === 'function') { callback = channelName; channelName = parentDevice; parentDevice = ''; } if (parentDevice && !channelName) { channelName = parentDevice; parentDevice = ''; } else if (parentDevice && typeof channelName === 'function') { callback = channelName; channelName = parentDevice; parentDevice = ''; } if (!adapterObjects) { this.log.info('deleteChannel not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!parentDevice) { parentDevice = ''; } const _parentDevice = parentDevice; const _channelName = channelName; if (parentDevice) { if (this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (channelName && this._namespaceRegExp.test(channelName)) { channelName = channelName.substring(this.namespace.length + 1); } if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { channelName = channelName.substring(parentDevice.length + 1); } channelName = channelName || ''; channelName = channelName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); channelName = this.namespace + '.' + this._DCS2ID(parentDevice, channelName); logger.info(this.namespaceLog + ' Delete channel ' + channelName); adapterObjects.getObjectView('system', 'channel', {startkey: channelName, endkey: channelName}, options, (err, res) => { if (err || !res || !res.rows) { return tools.maybeCallbackWithError(callback, err); } let cnt = 0; res.rows.length > 1 && logger.warn(this.namespaceLog + ' Found more than one channel ' + channelName); for (let t = 0; t < res.rows.length; t++) { cnt++; this.delObject(res.rows[t].id, options, err => { if (err) { return tools.maybeCallbackWithError(callback, err); } if (!--cnt) { adapterObjects.getObjectView('system', 'state', {startkey: channelName + '.', endkey: channelName + '.\u9999'}, options, (err, res) => { if (err || !res || !res.rows) { return tools.maybeCallbackWithError(callback, err); } let _cnt = 0; for (let k = 0; k < res.rows.length; k++) { _cnt++; this.deleteState(_parentDevice, _channelName, res.rows[k].id, options, err => { if (!--_cnt && callback) { callback(err); } else { if (err) { typeof callback === 'function' && callback(err); callback = null; } } }); } if (!_cnt && callback) { callback(); } }); } }); } if (!cnt && callback) { callback(); } }); }; /** * Promise-version of Adapter.deleteChannel */ this.deleteChannelAsync = tools.promisify(this.deleteChannel, this); this.deleteState = (parentDevice, parentChannel, stateName, options, callback) => { if (typeof parentChannel === 'function' && stateName === undefined) { stateName = parentDevice; callback = parentChannel; parentChannel = ''; parentDevice = ''; } else if (parentChannel === undefined && stateName === undefined) { stateName = parentDevice; parentDevice = ''; parentChannel = ''; } else { if (typeof options === 'function') { callback = options; options = null; } if (typeof stateName === 'function') { callback = stateName; stateName = parentChannel; parentChannel = parentDevice; parentDevice = ''; } if (typeof parentChannel === 'function') { callback = parentChannel; stateName = parentDevice; parentChannel = ''; parentDevice = ''; } if (typeof parentChannel === 'function') { callback = parentChannel; stateName = parentDevice; parentChannel = ''; parentDevice = ''; } } if (parentDevice) { if (this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (parentChannel) { if (this._namespaceRegExp.test(parentChannel)) { parentChannel = parentChannel.substring(this.namespace.length + 1); } if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { parentChannel = parentChannel.substring(parentDevice.length + 1); } parentChannel = parentChannel.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (this._namespaceRegExp.test(stateName)) { stateName = stateName.substring(this.namespace.length + 1); } if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { stateName = stateName.substring(parentDevice.length + 1); } if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { stateName = stateName.substring(parentChannel.length + 1); } stateName = stateName || ''; stateName = stateName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const _name = this._DCS2ID(parentDevice, parentChannel, stateName); this.delObject(_name, options, callback); }; /** * Promise-version of Adapter.deleteState */ this.deleteStateAsync = tools.promisify(this.deleteState, this); this.getDevices = (options, callback) => { if (typeof options === 'function' && typeof callback === 'object') { const tmp = callback; callback = options; options = tmp; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('getDevices not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.getObjectView('system', 'device', {startkey: this.namespace + '.', endkey: this.namespace + '.\u9999'}, options, (err, obj) => { if (err || !obj || !obj.rows || !obj.rows.length) { return tools.maybeCallbackWithError(callback, err, err ? undefined : []); } const res = []; for (let i = 0; i < obj.rows.length; i++) { res.push(obj.rows[i].value); } return tools.maybeCallbackWithError(callback, null, res); }); }; /** * Promise-version of Adapter.getDevices */ this.getDevicesAsync = tools.promisify(this.getDevices, this); this.getChannelsOf = (parentDevice, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (typeof parentDevice === 'function') { callback = parentDevice; parentDevice = null; } if (!adapterObjects) { this.log.info('getChannelsOf not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!parentDevice) { parentDevice = ''; } if (parentDevice && this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); parentDevice = this.namespace + (parentDevice ? ('.' + parentDevice) : ''); adapterObjects.getObjectView('system', 'channel', {startkey: parentDevice + '.', endkey: parentDevice + '.\u9999'}, options, (err, obj) => { if (err || !obj || !obj.rows || !obj.rows.length) { return tools.maybeCallbackWithError(callback, err, err ? undefined : []); } const res = []; for (let i = 0; i < obj.rows.length; i++) { res.push(obj.rows[i].value); } return tools.maybeCallbackWithError(callback, null, res); }); }; /** * Promise-version of Adapter.getChannelsOf */ this.getChannelsOfAsync = tools.promisify(this.getChannelsOf, this); this.getChannels = this.getChannelsOf; this.getChannelsAsync = this.getChannelsOfAsync; this.getStatesOf = (parentDevice, parentChannel, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (typeof parentDevice === 'function') { callback = parentDevice; parentDevice = null; parentChannel = null; } if (typeof parentChannel === 'function') { callback = parentChannel; parentChannel = null; } if (!callback) { return; } if (!adapterObjects) { this.log.info('getStatesOf not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!parentDevice) { parentDevice = ''; } else { if (this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (!parentChannel) { parentChannel = ''; } else if (this._namespaceRegExp.test(parentChannel)) { parentChannel = parentChannel.substring(this.namespace.length + 1); } if (parentDevice && parentChannel && parentChannel.substring(0, parentDevice.length) === parentDevice) { parentChannel = parentChannel.substring(parentDevice.length + 1); } parentChannel = parentChannel.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const id = this.namespace + '.' + this._DCS2ID(parentDevice, parentChannel, true); adapterObjects.getObjectView('system', 'state', {startkey: id, endkey: id + '\u9999'}, options, (err, obj) => { if (err || !obj || !obj.rows || !obj.rows.length) { return tools.maybeCallbackWithError(callback, err, err ? undefined : []); } const res = []; let read = 0; for (let i = 0; i < obj.rows.length; i++) { read++; adapterObjects.getObject(obj.rows[i].id, (err, subObj) => { if (subObj) { res.push(subObj); } if (!--read) { return tools.maybeCallbackWithError(callback, null, res); } }); } }); }; /** * Promise-version of Adapter.getStatesOf */ this.getStatesOfAsync = tools.promisify(this.getStatesOf, this); this.addStateToEnum = (enumName, addTo, parentDevice, parentChannel, stateName, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('addStateToEnum not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (parentDevice) { if (this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (parentChannel) { if (this._namespaceRegExp.test(parentChannel)) { parentChannel = parentChannel.substring(this.namespace.length + 1); } if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { parentChannel = parentChannel.substring(parentDevice.length + 1); } parentChannel = parentChannel.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (this._namespaceRegExp.test(stateName)) { stateName = stateName.substring(this.namespace.length + 1); } if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { stateName = stateName.substring(parentDevice.length + 1); } if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { stateName = stateName.substring(parentChannel.length + 1); } stateName = stateName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const objId = this._fixId({device: parentDevice, channel: parentChannel, state: stateName}); if (addTo.match(/^enum\./)) { adapterObjects.getObject(addTo, options, (err, obj) => { if (err || !obj) { return tools.maybeCallbackWithError(callback, err || tools.ERRORS.ERROR_NOT_FOUND); } const pos = obj.common.members.indexOf(objId); if (pos === -1) { obj.common.members.push(objId); obj.from = 'system.adapter.' + this.namespace; obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = Date.now(); adapterObjects.setObject(obj._id, obj, options, callback); } else { return tools.maybeCallback(callback); } }); } else { if (enumName.match(/^enum\./)) { enumName = enumName.substring(5); } adapterObjects.getObject('enum.' + enumName + '.' + addTo, options, (err, obj) => { if (!err && obj) { const pos = obj.common.members.indexOf(objId); if (pos === -1) { obj.common.members.push(objId); obj.from = 'system.adapter.' + this.namespace; obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = Date.now(); adapterObjects.setObject(obj._id, obj, callback); } else { return tools.maybeCallback(callback); } } else { if (err) { return tools.maybeCallbackWithError(callback, err); } // Create enum adapterObjects.setObject('enum.' + enumName + '.' + addTo, { common: { name: addTo, members: [objId] }, from: 'system.adapter.' + this.namespace, ts: Date.now(), type: 'enum' }, options, callback); } }); } }; /** * Promise-version of Adapter.addStateToEnum */ this.addStateToEnumAsync = tools.promisify(this.addStateToEnum, this); this.deleteStateFromEnum = (enumName, parentDevice, parentChannel, stateName, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('deleteStateFromEnum not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (parentDevice) { if (this._namespaceRegExp.test(parentDevice)) { parentDevice = parentDevice.substring(this.namespace.length + 1); } parentDevice = parentDevice.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (parentChannel) { if (this._namespaceRegExp.test(parentChannel)) { parentChannel = parentChannel.substring(this.namespace.length + 1); } if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { parentChannel = parentChannel.substring(parentDevice.length + 1); } parentChannel = parentChannel.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); } if (this._namespaceRegExp.test(stateName)) { stateName = stateName.substring(this.namespace.length + 1); } if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { stateName = stateName.substring(parentDevice.length + 1); } if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { stateName = stateName.substring(parentChannel.length + 1); } stateName = stateName.replace(FORBIDDEN_CHARS, '_').replace(/\./g, '_'); const objId = this._fixId({device: parentDevice, channel: parentChannel, state: stateName}, false/*, 'state'*/); if (enumName) { enumName = 'enum.' + enumName + '.'; } else { enumName = 'enum.'; } adapterObjects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, async (err, res) => { if (err || !res || !res.rows) { return tools.maybeCallbackWithError(callback, err); } for (const row of res.rows) { try { const obj = await adapterObjects.getObjectAsync(row.id); if (obj && obj.common && obj.common.members) { const pos = obj.common.members.indexOf(objId); if (pos !== -1) { obj.common.members.splice(pos, 1); obj.from = 'system.adapter.' + this.namespace; obj.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; obj.ts = Date.now(); await adapterObjects.setObjectAsync(obj._id, obj); } } } catch (e) { return tools.maybeCallbackWithError(callback, e); } } return tools.maybeCallback(callback); }); }; /** * Promise-version of Adapter.deleteStateFromEnum */ this.deleteStateFromEnumAsync = tools.promisify(this.deleteStateFromEnum, this); /** * Change file access rights * * This function updates the file access rights * <pre><code> * adapter.chmodFile('vis.0', '/main/vis-views.json', {mode: 0x644}, function (err, processed) { * if (err) adapter.log.error('Cannot read file: ' + err); * console.log('New files: ' + JSON.stringify(processed)); * }); * </code></pre> * * @alias chownFile * @memberof Adapter * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. * @param {string} path path to file without adapter name. E.g. If you want to update "/vis.0/main/*", here must be "/main/*" and _adapter must be equal to "vis.0". * @param {object} options data with mode * @param {function} callback return result * <pre><code> * function (err, processedFiles) { * list of processed files with new groups * } * </code></pre> */ this.chmodFile = (_adapter, path, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('chmodFile not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.chmodFile(_adapter, path, options, callback); }; /** * Promise-version of Adapter.chmodFile */ this.chmodFileAsync = tools.promisify(this.chmodFile, this); /** * Change file owner * * This function updates the file owner and ownerGroup * <pre><code> * adapter.chownFile('vis.0', '/main/vis-views.json', {owner: 'newOwner', ownerGroup: 'newgroup'}, function (err, processed) { * if (err) adapter.log.error('Cannot read file: ' + err); * console.log('New files: ' + JSON.stringify(processed)); * }); * </code></pre> * * @alias chownFile * @memberof Adapter * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. * @param {string} path path to file without adapter name. E.g. If you want to update "/vis.0/main/*", here must be "/main/*" and _adapter must be equal to "vis.0". * @param {object} options data with owner and ownerGroup * @param {function} callback return result * <pre><code> * function (err, processedFiles) { * list of processed files with new groups * } * </code></pre> */ this.chownFile = (_adapter, path, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('chownFile not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.chownFile(_adapter, path, options, callback); }; /** * Promise-version of Adapter.chownFile */ this.chownFileAsync = tools.promisify(this.chownFile, this); /** * Read directory from DB. * * This function reads the content of directory from DB for given adapter and path. * If getEnum called with no enum specified, all enums will be returned: * <pre><code> * adapter.readDir('vis.0', '/main/', function (err, filesOrDirs) { * // All enums * if (err) adapter.log.error('Cannot read directory: ' + err); * if (filesOrDirs) { * for (var f = 0; f < filesOrDirs.length; f++) { * adapter.log.debug('Directory main has following files and dirs: ' + filesOrDirs[f].file + '[dir - ' + filesOrDirs[f].isDir + ']'); * } * } * }); * </code></pre> * * @alias readDir * @memberof Adapter * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. * @param {string} path path to direcory without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". * @param {object} options optional user context * @param {ioBroker.ReadDirCallback} callback return result * <pre><code> * function (err, filesOrDirs) { * // filesOrDirs is array with elements like * // { * // file: 'views.json, * // stats: node.js stats object like https://nodejs.org/api/fs.html#fs_class_fs_stats , * // isDir: true/false, * // acl: access control list object, * // modifiedAt: time when modified, * // createdAt: time when created * // } * } * </code></pre> */ this.readDir = (_adapter, path, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('readDir not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.readDir(_adapter, path, options, callback); }; /** * Promise-version of Adapter.readDir */ this.readDirAsync = tools.promisify(this.readDir, this); this.unlink = (_adapter, name, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('unlink not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.unlink(_adapter, name, options, callback); }; /** * Promise-version of Adapter.unlink */ this.unlinkAsync = tools.promisify(this.unlink, this); this.delFile = this.unlink; this.delFileAsync = this.unlinkAsync; this.rename = (_adapter, oldName, newName, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('rename not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.rename(_adapter, oldName, newName, options, callback); }; /** * Promise-version of Adapter.rename */ this.renameAsync = tools.promisify(this.rename, this); this.mkdir = (_adapter, dirname, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('mkdir not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.mkdir(_adapter, dirname, options, callback); }; /** * Promise-version of Adapter.mkdir */ this.mkdirAsync = tools.promisify(this.mkdir, this); /** * Read file from DB. * * This function reads the content of one file from DB for given adapter and file name. * <pre><code> * adapter.readFile('vis.0', '/main/vis-views.json', function (err, data) { * // All enums * if (err) adapter.log.error('Cannot read file: ' + err); * console.log('Content of file is: ' + data); * }); * </code></pre> * * @alias readFile * @memberof Adapter * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. * @param {string} filename path to file without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". * @param {object} options optional user context * @param {ioBroker.ReadFileCallback} callback return result * <pre><code> * function (err, data) { * // data is utf8 or binary Buffer depends on the file extension. * } * </code></pre> */ this.readFile = (_adapter, filename, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('readFile not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.readFile(_adapter, filename, options, callback); }; /** * Promise-version of Adapter.readFile */ this.readFileAsync = tools.promisify(this.readFile, this, ['file', 'mimeType']); /** * Write file to DB. * * This function writes the content of one file into DB for given adapter and file name. * <pre><code> * adapter.writeFile('vis.0', '/main/vis-views.json', data, function (err) { * err && adapter.log.error('Cannot write file: ' + err); * }); * </code></pre> * * @alias readFile * @memberof Adapter * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. * @param {string} filename path to file without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". * @param {object} data data as UTF8 string or buffer depends on the file extension. * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] return result * <pre><code> * function (err) { * * } * </code></pre> */ this.writeFile = (_adapter, filename, data, options, callback) => { if (_adapter === null) { _adapter = this.name; } if (typeof options === 'function') { callback = options; options = null; } if (!adapterObjects) { this.log.info('writeFile not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } return adapterObjects.writeFile(_adapter, filename, data, options, callback); }; /** * Promise-version of Adapter.writeFile */ this.writeFileAsync = tools.promisify(this.writeFile, this); /** * Checks if file exists in DB. * * @alias fileExists * @memberof Adapter * @param {string} _adapter adapter name * @param {string} filename path to file without adapter name. E.g. If you want to check "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". * @param {object} [options] optional user context * @param {function} [callback] cb function if none provided, a promise is returned * @returns {Promise<boolean>} */ this.fileExists = async (_adapter, filename, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (typeof callback !== 'function') { return new Promise((resolve, reject) => { this.fileExists(_adapter, filename, options, (err, existent) => { if (err) { reject(err); } else { resolve(existent); } }); }); } if (!adapterObjects) { this.log.info('fileExists not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { const exists = await adapterObjects.fileExists(_adapter, filename, options); callback(null, exists); } catch(e) { callback(e); } }; /** * Promise-version of Adapter.fileExists */ this.fileExistsAsync = tools.promisify(this.fileExists, this); this.formatValue = (value, decimals, _format) => { if (typeof decimals !== 'number') { _format = decimals; decimals = 2; } const format = (!_format || _format.length !== 2) ? ((this.isFloatComma === undefined) ? '.,' : ((this.isFloatComma) ? '.,' : ',.')) : _format; if (typeof value !== 'number') { value = parseFloat(value); } return isNaN(value) ? '' : value.toFixed(decimals).replace(format[0], format[1]).replace(/\B(?=(\d{3})+(?!\d))/g, format[0]); }; this.formatDate = (dateObj, isDuration, _format) => { if ((typeof isDuration === 'string' && isDuration.toLowerCase() === 'duration') || isDuration === true) { isDuration = true; } if (typeof isDuration !== 'boolean') { _format = isDuration; isDuration = false; } if (!dateObj) { return ''; } const type = typeof dateObj; if (type === 'string') { dateObj = new Date(dateObj); } if (type !== 'object') { const j = parseInt(dateObj, 10); if (j === dateObj) { // may this is interval if (j < 946681200) { isDuration = true; dateObj = new Date(dateObj); } else { // if less 2000.01.01 00:00:00 dateObj = (j < 946681200000) ? new Date(j * 1000) : new Date(j); } } else { dateObj = new Date(dateObj); } } const format = _format || this.dateFormat || 'DD.MM.YYYY'; if (isDuration) { dateObj.setMilliseconds(dateObj.getMilliseconds() + dateObj.getTimezoneOffset() * 60 * 1000); } const validFormatChars = 'YJГMМDTДhSчmмsс'; let s = ''; let result = ''; const put = s => { /** @type {number | string} */ let v = ''; switch (s) { case 'YYYY': case 'JJJJ': case 'ГГГГ': case 'YY': case 'JJ': case 'ГГ': v = /** @type {Date} */(dateObj).getFullYear(); if (s.length === 2) { v %= 100; } if (v <= 9) { v = '0' + v; } break; case 'MM': case 'M': case 'ММ': case 'М': v = dateObj.getMonth() + 1; if (v < 10 && s.length === 2) { v = '0' + v; } break; case 'DD': case 'TT': case 'D': case 'T': case 'ДД': case 'Д': v = dateObj.getDate(); if (v < 10 && s.length === 2) { v = '0' + v; } break; case 'hh': case 'SS': case 'h': case 'S': case 'чч': case 'ч': v = dateObj.getHours(); if (v < 10 && s.length === 2) { v = '0' + v; } break; case 'mm': case 'm': case 'мм': case 'м': v = dateObj.getMinutes(); if (v < 10 && s.length === 2) { v = '0' + v; } break; case 'ss': case 's': case 'cc': case 'c': v = dateObj.getSeconds(); if (v < 10 && s.length === 2) { v = '0' + v; } v = v.toString(); break; case 'sss': case 'ссс': v = dateObj.getMilliseconds(); if (v < 10) { v = '00' + v; } else if (v < 100) { v = '0' + v; } v = v.toString(); } return result += v; }; for (let i = 0; i < format.length; i++) { if (validFormatChars.indexOf(format[i]) >= 0) { s += format[i]; } else { put(s); s = ''; result += format[i]; } } put(s); return result; }; }; const getGroups = (ids, callback, i) => { i = i || 0; if (!ids || i >= ids.length) { return tools.maybeCallback(callback); } else if (this.groups[ids] !== undefined) { setImmediate(getGroups, ids, callback, i + 1); } else { this.getForeignObject(ids[i], null, (err, obj) => { this.groups[ids] = obj || {}; setImmediate(getGroups, ids, callback, i + 1); }); } }; // Cache will be cleared if user or group changes.. Important! only if subscribed. const getUserGroups = (options, callback) => { if (this.users[options.user]) { options.groups = this.users[options.user].groups; options.acl = this.users[options.user].acl; return tools.maybeCallback(callback, options); } options.groups = []; this.getForeignObject(options.user, null, (err, userAcl) => { if (!userAcl) { // User does not exists logger.error(this.namespaceLog + ' unknown user "' + options.user + '"'); return tools.maybeCallback(callback, options); } else { this.getForeignObjects('*', 'group', null, null, (err, groups) => { // aggregate all groups permissions, where this user is if (groups) { for (const g in groups) { if (Object.prototype.hasOwnProperty.call(groups, g) && groups[g] && groups[g].common && groups[g].common.members && groups[g].common.members.indexOf(options.user) !== -1) { options.groups.push(groups[g]._id); } } } // read all groups for this user this.users[options.user] = {groups: options.groups, acl: (userAcl.common && userAcl.common.acl) || {}}; getGroups(options.groups, () => { // combine all rights const user = this.users[options.user]; for (let g = 0; g < options.groups.length; g++) { const gName = options.groups[g]; if (!this.groups[gName] || !this.groups[gName].common || !this.groups[gName].common.acl) { continue; } const group = this.groups[gName]; if (group.common.acl && group.common.acl.file) { if (!user.acl || !user.acl.file) { user.acl = user.acl || {}; user.acl.file = user.acl.file || {}; user.acl.file.create = group.common.acl.file.create; user.acl.file.read = group.common.acl.file.read; user.acl.file.write = group.common.acl.file.write; user.acl.file['delete'] = group.common.acl.file['delete']; user.acl.file.list = group.common.acl.file.list; } else { user.acl.file.create = user.acl.file.create || group.common.acl.file.create; user.acl.file.read = user.acl.file.read || group.common.acl.file.read; user.acl.file.write = user.acl.file.write || group.common.acl.file.write; user.acl.file['delete'] = user.acl.file['delete'] || group.common.acl.file['delete']; user.acl.file.list = user.acl.file.list || group.common.acl.file.list; } } if (group.common.acl && group.common.acl.object) { if (!user.acl || !user.acl.object) { user.acl = user.acl || {}; user.acl.object = user.acl.object || {}; user.acl.object.create = group.common.acl.object.create; user.acl.object.read = group.common.acl.object.read; user.acl.object.write = group.common.acl.object.write; user.acl.object['delete'] = group.common.acl.object['delete']; user.acl.object.list = group.common.acl.object.list; } else { user.acl.object.create = user.acl.object.create || group.common.acl.object.create; user.acl.object.read = user.acl.object.read || group.common.acl.object.read; user.acl.object.write = user.acl.object.write || group.common.acl.object.write; user.acl.object['delete'] = user.acl.object['delete'] || group.common.acl.object['delete']; user.acl.object.list = user.acl.object.list || group.common.acl.object.list; } } if (group.common.acl && group.common.acl.users) { if (!user.acl || !user.acl.users) { user.acl = user.acl || {}; user.acl.users = user.acl.users || {}; user.acl.users.create = group.common.acl.users.create; user.acl.users.read = group.common.acl.users.read; user.acl.users.write = group.common.acl.users.write; user.acl.users['delete'] = group.common.acl.users['delete']; user.acl.users.list = group.common.acl.users.list; } else { user.acl.users.create = user.acl.users.create || group.common.acl.users.create; user.acl.users.read = user.acl.users.read || group.common.acl.users.read; user.acl.users.write = user.acl.users.write || group.common.acl.users.write; user.acl.users['delete'] = user.acl.users['delete'] || group.common.acl.users['delete']; user.acl.users.list = user.acl.users.list || group.common.acl.users.list; } } if (group.common.acl && group.common.acl.state) { if (!user.acl || !user.acl.state) { user.acl = user.acl || {}; user.acl.state = user.acl.state || {}; user.acl.state.create = group.common.acl.state.create; user.acl.state.read = group.common.acl.state.read; user.acl.state.write = group.common.acl.state.write; user.acl.state['delete'] = group.common.acl.state['delete']; user.acl.state.list = group.common.acl.state.list; } else { user.acl.state.create = user.acl.state.create || group.common.acl.state.create; user.acl.state.read = user.acl.state.read || group.common.acl.state.read; user.acl.state.write = user.acl.state.write || group.common.acl.state.write; user.acl.state['delete'] = user.acl.state['delete'] || group.common.acl.state['delete']; user.acl.state.list = user.acl.state.list || group.common.acl.state.list; } } } options.acl = user.acl; return tools.maybeCallback(callback, options); }); }); } }); }; /** * This method update the cached values in this.usernames * * @returns {Promise<void>} */ const updateUsernameCache = async () => { // make sure cache is cleared try { // get all users const obj = await this.getObjectListAsync({startkey: 'system.user.', endkey: 'system.user.\u9999'}); this.usernames = {}; for (const row of obj.rows) { if (row.value.common && typeof row.value.common.name === 'string') { this.usernames[row.value.common.name] = {id: row.id.replace(FORBIDDEN_CHARS, '_')}; } else { logger.warn(`${this.namespaceLog} Invalid username for id "${row.id}"`); } } } catch (e) { throw new Error(`Could not update user cache: ${e.message}`); } }; const checkState = (obj, options, command) => { const limitToOwnerRights = options.limitToOwnerRights === true; if (obj && obj.acl) { obj.acl.state = obj.acl.state || obj.acl.object; if (obj.acl.state) { // If user is owner if (options.user === obj.acl.owner) { if (command === 'setState' || command === 'delState') { if (command === 'delState' && !options.acl.state['delete']) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } else if (command === 'setState' && !options.acl.state.write) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } else if (!(obj.acl.state & ACCESS_USER_WRITE)) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } } else if (command === 'getState') { if (!(obj.acl.state & ACCESS_USER_READ) || !options.acl.state.read) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } } else { logger.warn(this.namespaceLog + ' Called unknown command:' + command); } } else if (options.groups.indexOf(obj.acl.ownerGroup) !== -1 && !limitToOwnerRights) { if (command === 'setState' || command === 'delState') { if (command === 'delState' && !options.acl.state['delete']) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } else if (command === 'setState' && !options.acl.state.write) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } else if (!(obj.acl.state & ACCESS_GROUP_WRITE)) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } } else if (command === 'getState') { if (!(obj.acl.state & ACCESS_GROUP_READ) || !options.acl.state.read) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } } else { logger.warn(this.namespaceLog + ' Called unknown command:' + command); } } else if (!limitToOwnerRights) { if (command === 'setState' || command === 'delState') { if (command === 'delState' && !options.acl.state['delete']) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } else if (command === 'setState' && !options.acl.state.write) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user} on "${obj._id}": ${command}`); return false; } else if (!(obj.acl.state & ACCESS_EVERY_WRITE)) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user}" on "${obj._id}": ${command}`); return false; } } else if (command === 'getState') { if (!(obj.acl.state & ACCESS_EVERY_READ) || !options.acl.state.read) { logger.warn(`${this.namespaceLog} Permission error for user "${options.user}"on "${obj._id}" : ${command}`); return false; } } else { logger.warn(this.namespaceLog + ' Called unknown command:' + command); return false; } } else { logger.warn(this.namespaceLog + ' Permissions limited to Owner rights'); return false; } } else if (limitToOwnerRights) { logger.warn(this.namespaceLog + ' Permissions limited to Owner rights'); return false; } } else if (limitToOwnerRights) { logger.warn(this.namespaceLog + ' Permissions limited to Owner rights'); return false; } return true; }; const checkStates = (ids, options, command, callback, _helper) => { if (!options.groups) { return getUserGroups(options, () => checkStates(ids, options, command, callback)); } if (Array.isArray(ids)) { if (!ids.length) { return tools.maybeCallbackWithError(callback, null, ids); } if (options._objects) { const ids = []; const objs = []; options._objects.forEach((obj, i) => { if (obj && checkState(options._objects[i], options, command)) { ids.push(obj._id); objs.push(obj); } }); options._objects = undefined; return tools.maybeCallbackWithError(callback, null, ids, objs); } else { _helper = _helper || { i: 0, objs: options._objects || [], errors: [] }; // this must be a serial call checkStates(ids[_helper.i], options, command, (err, obj) => { if (err && obj) { _helper.errors.push(obj._id); } if (obj) { _helper.objs[_helper.i] = obj; } // if finished if (_helper.i + 1 >= ids.length) { if (_helper.errors.length) { for (let j = ids.length - 1; j >= 0; j--) { if (_helper.errors.indexOf(ids[j]) !== -1) { ids.splice(j, 1); _helper.objs.splice(j, 1); } } } return tools.maybeCallbackWithError(callback, null, ids, _helper.objs); } else { _helper.i++; setImmediate(() => checkStates(ids, options, command, callback, _helper)); } }); } } else { let originalChecked = undefined; if (options.checked !== undefined) { originalChecked = options.checked; } options.checked = true; if (!adapterObjects) { this.log.info('checkStates not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.getObject(ids, options, (err, obj) => { if (originalChecked !== undefined) { options.checked = originalChecked; } else { options.checked = undefined; } if (err) { return tools.maybeCallbackWithError(callback, err, {_id: ids}); } else { if (!checkState(obj, options, command)) { return tools.maybeCallbackWithError(callback, ERROR_PERMISSION, {_id: ids}); } } return tools.maybeCallbackWithError(callback, null, obj); }); } }; // find out default history instance const getDefaultHistory = callback => { if (!this.defaultHistory) { // read default history instance from system.config return this.getForeignObject('system.config', null, (err, data) => { if (data && data.common) { this.defaultHistory = data.common.defaultHistory; } if (data && data.native) { systemSecret = data.native.secret; } // if no default history set if (!this.defaultHistory) { // read all adapters adapterObjects.getObjectView('system', 'instance', {startkey: '', endkey: '\u9999'}, (err, _obj) => { if (_obj && _obj.rows) { for (let i = 0; i < _obj.rows.length; i++) { if (_obj.rows[i].value.common && _obj.rows[i].value.common.type === 'storage') { this.defaultHistory = _obj.rows[i].id.substring('system.adapter.'.length); break; } } } if (!this.defaultHistory) { this.defaultHistory = 'history.0'; } return tools.maybeCallback(callback); }); } else { return tools.maybeCallback(callback); } }); } else { return tools.maybeCallback(callback); } }; const _setStateChangedHelper = (id, state, callback) => { if (!adapterObjects) { this.log.info('setStateChanged not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (id.startsWith(ALIAS_STARTS_WITH)) { adapterObjects.getObject(id, (err, obj) => { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // id can be string or can have attribute write const aliasId = typeof obj.common.alias.id.write === 'string' ? obj.common.alias.id.write : obj.common.alias.id; _setStateChangedHelper(aliasId, state, callback); } else { logger.warn(`${this.namespaceLog} ${err ? err.message : `Alias ${id} has no target 1`}`); return tools.maybeCallbackWithError(callback, err ? err.message : `Alias ${id} has no target`); } }); } else { this.getForeignState(id, null, async (err, oldState) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { let differ = false; if (!oldState) { differ = true; } else if (state.val !== oldState.val) { differ = true; } else if (state.ack !== undefined && state.ack !== oldState.ack) { differ = true; } else if (state.q !== undefined && state.q !== oldState.q) { differ = true; } else if (state.ts !== undefined && state.ts !== oldState.ts) { differ = true; } else // if comment changed if (state.c !== undefined && state.c !== oldState.c) { differ = true; } else if (state.expire !== undefined && state.expire !== oldState.expire) { differ = true; } else if (state.from !== undefined && state.from !== oldState.from) { differ = true; } else if (state.user !== undefined && state.user !== oldState.user) { differ = true; } if (differ) { if (this.performStrictObjectChecks) { // validate that object exists, read-only logic ok, type ok, etc. won't throw now await this._performStrictObjectCheck(id, state); } this.outputCount++; adapterStates.setState(id, state, (/* err */) => { return tools.maybeCallbackWithError(callback, null, id, false); }); } else { return tools.maybeCallbackWithError(callback, null, id, true); } } }); } }; // initStates is called from initAdapter const initStates = cb => { logger.debug(this.namespaceLog + ' objectDB connected'); config.states.maxQueue = config.states.maxQueue || 1000; initializeTimeout = setTimeout(() => { initializeTimeout = null; if (config.isInstall) { logger && logger.warn(this.namespaceLog + ' no connection to states DB. Terminating.'); this.terminate(EXIT_CODES.NO_ERROR); } else { logger && logger.warn(this.namespaceLog + ' slow connection to states DB. Still waiting ...'); } }, config.states.connectTimeout || 2000); // Internal object, but some special adapters want to access it anyway. adapterStates = new States({ namespace: this.namespaceLog, connection: config.states, connected: async _statesInstance => { logger.debug(this.namespaceLog + ' statesDB connected'); this.statesConnectedTime = Date.now(); if (initializeTimeout) { clearTimeout(initializeTimeout); initializeTimeout = null; } if (!config.isInstall) { // Subscribe for process exit signal adapterStates.subscribe(`system.adapter.${this.namespace}.sigKill`); // Subscribe for loglevel adapterStates.subscribe(`system.adapter.${this.namespace}.logLevel`); } if (options.subscribable) { // subscribe on if other instance wants to have states of this adapter adapterStates.subscribe(`system.adapter.${this.namespace}.subscribes`); // read actual autosubscribe requests let state; try { state = await adapterStates.getStateAsync(`system.adapter.${this.namespace}.subscribes`); } catch { // ignore } if (!state || !state.val) { this.patterns = {}; } else { try { this.patterns = JSON.parse(state.val); Object.keys(this.patterns).forEach(p => this.patterns[p].regex = tools.pattern2RegEx(p)); } catch { this.patterns = {}; } } return tools.maybeCallback(cb); } else { return tools.maybeCallback(cb); } }, logger: logger, change: (id, state) => { this.inputCount++; if (state === 'null' || state === '') { state = null; } if (!id || typeof id !== 'string') { console.log('Something is wrong! ' + JSON.stringify(id)); return; } if (id === 'system.adapter.' + this.namespace + '.sigKill' && state && state.ts > this.statesConnectedTime && state.from && state.from.startsWith('system.host.')) { const sigKillVal = parseInt(state.val); if (!isNaN(sigKillVal)) { if (this.startedInCompactMode || sigKillVal === -1) { logger.info(this.namespaceLog + ' Got terminate signal ' + (sigKillVal === -1 ? 'TERMINATE_YOURSELF' : (' TERMINATE ' + sigKillVal))); } else { logger.warn(this.namespaceLog + ' Got terminate signal. Checking desired PID: ' + sigKillVal + ' vs own PID ' + process.pid); } // by deletion of state, stop this instance if ((sigKillVal !== process.pid) && !config.forceIfDisabled) { stop(false, false, EXIT_CODES.ADAPTER_REQUESTED_TERMINATION, false); setTimeout(() => this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION), 4000); } } } if (id === `system.adapter.${this.namespace}.logLevel`) { if (config && config.log && state && !state.ack) { let currentLevel = config.log.level; if (state.val && state.val !== currentLevel && ['silly', 'debug', 'info', 'warn', 'error'].includes(state.val)) { this.overwriteLogLevel = true; config.log.level = state.val; for (const transport in logger.transports) { if (!Object.prototype.hasOwnProperty.call(logger.transports, transport)) { continue; } logger.transports[transport].level = state.val; } logger.info(`${this.namespaceLog} Loglevel changed from "${currentLevel}" to "${state.val}"`); currentLevel = state.val; } else if (state.val && state.val !== currentLevel) { logger.info(`${this.namespaceLog} Got invalid loglevel "${state.val}", ignoring`); } this.outputCount++; adapterStates && adapterStates.setState(`system.adapter.${this.namespace}.logLevel`, { val: currentLevel, ack: true, from: `system.adapter.${this.namespace}` }); } } // todo remove it as an error with log will be found if (id === `system.adapter.${this.namespace}.checkLogging`) { checkLogging(); } // someone subscribes or unsubscribes from adapter if (options.subscribable && id === `system.adapter.${this.namespace}.subscribes`) { let subs; try { subs = JSON.parse(state.val || '{}'); Object.keys(subs).forEach(p => subs[p].regex = tools.pattern2RegEx(p)); } catch { subs = {}; } this.patterns = subs; if (!stopInProgress) { if (typeof options.subscribesChange === 'function') { options.subscribesChange(subs); } else { this.emit('subscribesChange', subs); } } } // If someone want to have log messages if (this.logList && id.match(/\.logging$/)) { const instance = id.substring(0, id.length - '.logging'.length); logger && logger.debug(this.namespaceLog + ' ' + instance + ': logging ' + (state ? state.val : false)); this.logRedirect(state ? state.val : false, instance); } else if (id === `log.system.adapter.${this.namespace}`) { options.logTransporter && this.processLog && this.processLog(state); } else // If this is messagebox if (id === `messagebox.system.adapter.${this.namespace}` && state) { const obj = state; if (obj) { // If callback stored for this request if (obj.callback && obj.callback.ack && obj.callback.id && this.callbacks && this.callbacks['_' + obj.callback.id]) { // Call callback function if (this.callbacks['_' + obj.callback.id].cb) { this.callbacks['_' + obj.callback.id].cb(obj.message); delete this.callbacks['_' + obj.callback.id]; } // delete too old callbacks IDs, like garbage collector const now = Date.now(); for (const _id in this.callbacks) { if (now - this.callbacks[_id].time > 3600000) { delete this.callbacks[_id]; } } } else if (!stopInProgress) { if (options.message) { // Else inform about new message the adapter options.message(obj); } this.emit('message', obj); } } } else if (id.startsWith('system.adapter.' + this.namespace + '.plugins.') && id.endsWith('.enabled')) { if (!state || state.ack) { return; } const pluginStatesIndex = ('system.adapter.' + this.namespace + '.plugins.').length; let nameEndIndex = id.indexOf('.', pluginStatesIndex + 1); if (nameEndIndex === -1) { nameEndIndex = undefined; } const pluginName = id.substring(pluginStatesIndex, nameEndIndex); if (!this.pluginHandler.pluginExists(pluginName)) { return; } if (this.pluginHandler.isPluginActive(pluginName) !== state.val) { if (state.val) { if (!this.pluginHandler.isPluginInstanciated(pluginName)) { this.pluginHandler.instanciatePlugin(pluginName, this.pluginHandler.getPluginConfig(pluginName), __dirname); this.pluginHandler.setDatabaseForPlugin(pluginName, adapterObjects, adapterStates); this.pluginHandler.initPlugin(pluginName, this.adapterConfig); } } else { if (!this.pluginHandler.destroy(pluginName)) { logger.info(this.namespaceLog + ' Plugin ' + pluginName + ' could not be disabled. Please restart adapter to disable it.'); } } } } else // If adapter is ready and for this ID exist some alias links if (this.adapterReady && this.aliases[id]) { this.aliases[id].targets.forEach(target => { const aState = state ? tools.formatAliasValue(this.aliases[id].source, target, JSON.parse(JSON.stringify(state)), logger, this.namespaceLog) : null; const targetId = target.id.read === 'string' ? target.id.read : target.id; if (!stopInProgress && (aState || !state)) { if (typeof options.stateChange === 'function') { options.stateChange(targetId, aState); } else { // emit 'stateChange' event instantly setImmediate(() => this.emit('stateChange', targetId, aState)); } } }); } }, changeUser: (id, state) => { this.inputCount++; if (state === 'null' || state === '') { state = null; } if (!id || typeof id !== 'string') { console.log('Something is wrong! ' + JSON.stringify(id)); return; } if (this.adapterReady) { if (this.oStates) { if (!state) { delete this.oStates[id]; } else { this.oStates[id] = state; } } if (!stopInProgress) { if (typeof options.stateChange === 'function') { setImmediate(() => options.stateChange(id, state)); } else { // emit 'stateChange' event instantly setImmediate(() => this.emit('stateChange', id, state)); } } } }, disconnected: () => { this.connected = false; !this.terminated && setTimeout(() => { if (this.connected) { return; } // If reconnected in the meantime, do not terminate logger && logger.warn(this.namespaceLog + ' Cannot connect/reconnect to states DB. Terminating'); this.terminate(EXIT_CODES.NO_ERROR); }, 5000); } }); /** * Send message to other adapter instance or all instances of adapter. * * This function sends a message to specific instance or all instances of some specific adapter. * If no instance given (e.g. "pushover"), the callback argument will be ignored. Because normally many responses will come. * * @alias sendTo * @memberof Adapter * @param {string} instanceName name of the instance where the message must be send to. E.g. "pushover.0" or "system.adapter.pushover.0". * @param {string} command command name, like "send", "browse", "list". Command is depend on target adapter implementation. * @param {object} message object that will be given as argument for request * @param {function(any):any} [callback] optional return result * <pre><code> * function (result) { * // result is target adapter specific and can vary from adapter to adapter * if (!result) adapter.log.error('No response received'); * } * </code></pre> */ this.sendTo = async (instanceName, command, message, callback) => { if ((typeof message === 'function') && (typeof callback === 'undefined')) { callback = message; message = undefined; } if (typeof message === 'undefined') { message = command; command = 'send'; } const obj = {command: command, message: message, from: 'system.adapter.' + this.namespace}; if (typeof instanceName !== 'string' || !instanceName) { typeof callback === 'function' && setImmediate(() => callback('No instanceName provided or not a string')); return; } if (!instanceName.match(/^system\.adapter\./)) { instanceName = 'system.adapter.' + instanceName; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('sendTo not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (typeof message !== 'object') { logger.debug(this.namespaceLog + ' sendTo "' + command + '" to ' + instanceName + ' from system.adapter.' + this.namespace + ': ' + message); } else { logger.debug(this.namespaceLog + ' sendTo "' + command + '" to ' + instanceName + ' from system.adapter.' + this.namespace); } // If not specific instance if (!instanceName.match(/\.[0-9]+$/)) { if (!adapterObjects) { this.log.info('sendTo not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // Send to all instances of adapter adapterObjects.getObjectView('system', 'instance', { startkey: instanceName + '.', endkey: instanceName + '.\u9999' }, async (err, _obj) => { if (_obj && _obj.rows) { for (let i = 0; i < _obj.rows.length; i++) { try { await adapterStates.pushMessage(_obj.rows[i].id, obj); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } } }); } else { if (callback) { if (typeof callback === 'function') { // force subscribe even no messagebox enabled if (!this.common.messagebox && !this.mboxSubscribed) { this.mboxSubscribed = true; adapterStates.subscribeMessage('system.adapter.' + this.namespace); } obj.callback = { message: message, id: callbackId++, ack: false, time: Date.now() }; if (callbackId >= 0xFFFFFFFF) { callbackId = 1; } if (!this.callbacks) { this.callbacks = {}; } this.callbacks['_' + obj.callback.id] = {cb: callback}; // delete too old callbacks IDs const now = Date.now(); for (const _id in this.callbacks) { if (now - this.callbacks[_id].time > 3600000) { delete this.callbacks[_id]; } } } else { obj.callback = callback; obj.callback.ack = true; } } try { await adapterStates.pushMessage(instanceName, obj); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } }; /** * Promise-version of Adapter.sendTo */ this.sendToAsync = tools.promisifyNoError(this.sendTo, this); /** * Send message to specific host or to all hosts. * * This function sends a message to specific host or all hosts. * If no host name given (e.g. null), the callback argument will be ignored. Because normally many responses will come. * * @alias sendToHost * @memberof Adapter * @param {any} hostName name of the host where the message must be send to. E.g. "myPC" or "system.host.myPC". If argument is empty, the message will be sent to all hosts. * @param {string} command command name. One of: "cmdExec", "getRepository", "getInstalled", "getVersion", "getDiagData", "getLocationOnDisk", "getDevList", "getLogs", "delLogs", "readDirAsZip", "writeDirAsZip", "readObjectsAsZip", "writeObjectsAsZip", "checkLogging". Commands can be checked in controller.js (function processMessage) * @param {object} message object that will be given as argument for request * @param {function(any):any} [callback] optional return result * <pre><code> * function (result) { * // result is target adapter specific and can vary from command to command * if (!result) adapter.log.error('No response received'); * } * </code></pre> */ this.sendToHost = async (hostName, command, message, callback) => { if (typeof message === 'undefined') { message = command; command = 'send'; } const obj = {command, message, from: 'system.adapter.' + this.namespace}; if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('sendToHost not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (hostName && typeof hostName !== 'string') { hostName = hostName.toString(); } if (hostName && !hostName.startsWith('system.host.')) { hostName = 'system.host.' + hostName; } if (!hostName) { if (!adapterObjects) { this.log.info('sendToHost not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // Send to all hosts adapterObjects.getObjectList({startkey: 'system.host.', endkey: `system.host.\u9999`}, null, async (err, res) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe return; } if (!err && res.rows.length) { for (let i = 0; i < res.rows.length; i++) { const parts = res.rows[i].id.split('.'); // ignore system.host.name.alive and so on if (parts.length === 3) { try { await adapterStates.pushMessage(res.rows[i].id, obj); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } } } }); } else { if (callback) { if (typeof callback === 'function') { // force subscribe even no messagebox enabled if (!this.common.messagebox && !this.mboxSubscribed) { this.mboxSubscribed = true; adapterStates.subscribeMessage(`system.adapter.${this.namespace}`); } obj.callback = { message, id: callbackId++, ack: false, time: Date.now() }; if (callbackId >= 0xFFFFFFFF) { callbackId = 1; } this.callbacks = this.callbacks || {}; this.callbacks['_' + obj.callback.id] = {cb: callback}; } else { obj.callback = callback; obj.callback.ack = true; } } try { await adapterStates.pushMessage(hostName, obj); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } }; /** * Promise-version of Adapter.sendToHost */ this.sendToHostAsync = tools.promisifyNoError(this.sendToHost, this); /** * Send notification with given scope and category to host of this adapter * * @param {string} scope - scope to be addressed * @param {string|null} category - to be addressed, if null message will be checked by regex of given scope * @param {string} message - message to be stored/checked * @return Promise<void> */ this.registerNotification = async (scope, category, message) => { const obj = { command: 'addNotification', message: {scope, category, message, instance: this.namespace}, from: `system.adapter.${this.namespace}` }; await adapterStates.pushMessage(this.host, obj); }; /** * Validates the object-type argument that is passed to setState * @param {Record<string, any>} obj */ function validateSetStateObjectArgument(obj) { // Check that we have at least one existing non-undefined property at all, else invalid if (!Object.keys(obj).some(key => obj[key] !== undefined)) { throw new Error(`The state contains no properties! At least one property is expected!`); } /* The following object parameters and types are allowed: val: any, (optional) ack: boolean, (optional) ts: number, (optional) q: number, (optional) from: string, (optional) c: string, (optional) expire: number (optional) lc: number (optional) user: string (optional) Everything else is forbidden */ const optionalProperties = { val: 'any', ack: 'boolean', ts: 'number', q: 'number', from: 'string', c: 'string', expire: 'number', lc: 'number', user: 'string' }; // Are there any forbidden properties? const forbiddenProperties = Object.keys(obj).filter(k => !optionalProperties[k]); if (forbiddenProperties.length) { throw new Error(`The state contains the forbidden properties ${forbiddenProperties.join(', ')}!`); } // Do all properties have the correct type? for (const [key, type] of Object.entries(optionalProperties)) { // any permits all types if (type === 'any') { continue; } // don't flag optional properties when they don't exist or are undefined if (!(key in obj) || obj[key] === undefined) { continue; } if (type !== typeof obj[key]) { throw new Error(`The state property "${key}" has the wrong type "${typeof obj[key]}" (should be "${type}")!`); } } } /** * Writes value into states DB. * * This function can write values into states DB for this adapter. * Only Ids that belong to this adapter can be modified. So the function automatically adds "adapter.X." to ID. * ack, options and callback are optional * * @alias setState * @memberof Adapter * @param {string} id object ID of the state. * @param {object|string|number|boolean} state simple value or object with attribues. * If state is object and ack exists too as function argument, function argument has priority. * <pre><code> * { * val: value, * ack: true|false, // default - false; is command(false) or status(true) * ts: timestampMS, // default - now * q: qualityAsNumber, // default - 0 (ok) * from: origin, // default - this adapter * c: comment, // default - empty * expire: expireInSeconds // default - 0 * lc: timestampMS // default - automatic calculation * } * </code></pre> * @param {boolean} [ack] optional is command(false) or status(true) * @param {object} [options] optional user context * @param {ioBroker.SetStateCallback} [callback] optional return error and id * <pre><code> * function (err, id) { * if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err); * } * </code></pre> */ this.setState = async (id, state, ack, options, callback) => { if (typeof state === 'object' && typeof ack !== 'boolean') { callback = options; options = ack; ack = undefined; } if (typeof options === 'function') { callback = options; options = {}; } if (typeof ack === 'function') { callback = ack; ack = undefined; } if (!adapterStates) { // if states is no longer existing, we do not need to set this.log.info('setState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!adapterObjects) { this.log.info('setState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } id = this._fixId(id, false); if (tools.isObject(state)) { // Verify that the passed state object is valid try { validateSetStateObjectArgument(state); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } else { // wrap non-object values in a state object state = state !== undefined ? {val: state} : {}; } if (state.val === undefined && !Object.keys(state).length) { // undefined is not allowed as state.val -> return this.log.info(`undefined is not a valid state value for id "${id}"`); // TODO: reactivate line below + test in in next controller version (02.05.2021) // return tools.maybeCallbackWithError(callback, 'undefined is not a valid state value'); } if (ack !== undefined) { state.ack = ack; } // if state.from provided, we use it else, we set default property state.from = typeof state.from === 'string' && state.from !== '' ? state.from : `system.adapter.${this.namespace}`; state.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'setState', async (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (!adapterObjects) { // if objects is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (this.performStrictObjectChecks) { // validate that object exists, read-only logic ok, type ok, etc. won't throw now await this._performStrictObjectCheck(id, state); } if (id.startsWith(ALIAS_STARTS_WITH)) { // write alias if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // id can be string or can have attribute write const aliasId = typeof obj.common.alias.id.write === 'string' ? obj.common.alias.id.write : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } // check the rights checkStates(aliasId, options, 'setState', (err, targetObj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // write target state this.outputCount++; adapterStates.setState(aliasId, tools.formatAliasValue(obj && obj.common, targetObj && targetObj.common, state, logger, this.namespaceLog), callback); } }); } else { logger.warn(`${this.namespaceLog} ${`Alias ${id} has no target 2`}`); return tools.maybeCallbackWithError(callback, `Alias ${id} has no target`); } } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(id, state, callback); } } }); } else { if (id.startsWith(ALIAS_STARTS_WITH)) { // write alias // read alias id adapterObjects.getObject(id, options, (err, obj) => { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { const aliasId = typeof obj.common.alias.id.write === 'string' ? obj.common.alias.id.write : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } // read object for formatting adapterObjects.getObject(aliasId, options, (err, targetObj) => { // write target state this.outputCount++; adapterStates.setState(aliasId, tools.formatAliasValue(obj && obj.common, targetObj && targetObj.common, state, logger, this.namespaceLog), callback); }); } else { logger.warn(`${this.namespaceLog} ${err ? err.message : `Alias ${id} has no target 3`}`); return tools.maybeCallbackWithError(callback, err ? err.message : `Alias ${id} has no target`); } }); } else { if (this.performStrictObjectChecks) { // validate that object exists, read-only logic ok, type ok, etc. won't throw now await this._performStrictObjectCheck(id, state); } if (!adapterStates) { // if states is no longer existing, we do not need to set this.log.info('setState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(id, state, callback); } } }; /** * Promise-version of Adapter.setState */ this.setStateAsync = tools.promisify(this.setState, this); /** * Writes value into states DB only if the value really changed. * * This function can write values into states DB for this adapter. * Only Ids that belong to this adapter can be modified. So the function automatically adds "adapter.X." to ID. * ack, options and callback are optional * * @alias setStateChanged * @memberof Adapter * @param {string} id object ID of the state. * @param {object|string|number|boolean} state simple value or object with attribues. * @param {boolean} [ack] optional is command(false) or status(true) * @param {object} [options] optional user context * @param {ioBroker.SetStateChangedCallback} [callback] optional return error, id and notChanged * <pre><code> * function (err, id, notChanged) { * if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err); * if (!notChanged) adapter.log.debug('Value was changed'); * } * </code></pre> */ this.setStateChanged = (id, state, ack, options, callback) => { if (typeof state === 'object' && typeof ack !== 'boolean') { callback = options; options = ack; ack = undefined; } if (typeof options === 'function') { callback = options; options = {}; } if (typeof ack === 'function') { callback = ack; ack = undefined; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setStateCHanged not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } id = this._fixId(id, false/*, 'state'*/); if (tools.isObject(state)) { // Verify that the passed state object is valid try { validateSetStateObjectArgument(state); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } else { // wrap non-object values in a state object state = state !== undefined ? {val: state} : {}; } if (state.val === undefined && !Object.keys(state).length) { // undefined is not allowed as state.val -> return this.log.info(`undefined is not a valid state value for id "${id}"`); // TODO: reactivate line below + test in in next controller version (02.05.2021) // return tools.maybeCallbackWithError(callback, 'undefined is not a valid state value'); } if (ack !== undefined) { state.ack = ack; } // if state.from provided, we use it else, we set default property state.from = typeof state.from === 'string' && state.from !== '' ? state.from : `system.adapter.${this.namespace}`; if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'setState', err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { _setStateChangedHelper(id, state, callback); } }); } else { _setStateChangedHelper(id, state, callback); } }; /** * Promise-version of Adapter.setStateChanged */ this.setStateChangedAsync = tools.promisify(this.setStateChanged, this, ['id', 'notChanged']); /** * Writes value into states DB for any instance. * * This function can write values into states DB for all instances and system states too. * ack, options and callback are optional * * @alias setForeignState * @memberof Adapter * @param {string} id object ID of the state. * @param {object|string|number|boolean} state simple value or object with attribues. * If state is object, so the ack will be ignored and must be included into object. * <pre><code> * { * val: value, * ack: true|false, // default - false; is command(false) or status(true) * ts: timestampMS, // default - now * q: qualityAsNumber, // default - 0 (ok) * from: origin, // default - this adapter * c: comment, // default - empty * expire: expireInSeconds // default - 0 * lc: timestampMS // default - automatic calculation * } * </code></pre> * @param {boolean} [ack] optional is command(false) or status(true) * @param {object} [options] optional user context * @param {ioBroker.SetStateCallback} [callback] optional return error and id * <pre><code> * function (err, id) { * if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err); * } * </code></pre> */ this.setForeignState = async (id, state, ack, options, callback) => { if (typeof state === 'object' && typeof ack !== 'boolean') { callback = options; options = ack; ack = undefined; } if (typeof options === 'function') { callback = options; options = {}; } if (typeof ack === 'function') { callback = ack; ack = undefined; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } if (tools.isObject(state)) { // Verify that the passed state object is valid try { validateSetStateObjectArgument(state); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } else { // wrap non-object values in a state object state = state !== undefined ? {val: state} : {}; } if (state.val === undefined && !Object.keys(state).length) { // undefined is not allowed as state.val -> return this.log.info(`undefined is not a valid state value for id "${id}"`); // TODO: reactivate line below + test in in next controller version (02.05.2021) // return tools.maybeCallbackWithError(callback, 'undefined is not a valid state value'); } if (ack !== undefined) { state.ack = ack; } // if state.from provided, we use it else, we set default property state.from = typeof state.from === 'string' && state.from !== '' ? state.from : `system.adapter.${this.namespace}`; state.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; if (!id || typeof id !== 'string') { const warn = id ? `ID can be only string and not "${typeof id}"` : `Empty ID: ${JSON.stringify(state)}`; logger.warn(`${this.namespaceLog} ${warn}`); return tools.maybeCallbackWithError(callback, warn); } const mId = id.replace(FORBIDDEN_CHARS, '_'); if (mId !== id) { logger.warn(`${this.namespaceLog} Used invalid characters: ${id} changed to ${mId}`); id = mId; } if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'setState', async (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (this.performStrictObjectChecks) { // validate that object exists, read-only logic ok, type ok, etc. won't throw now await this._performStrictObjectCheck(id, state); } if (id.startsWith(ALIAS_STARTS_WITH)) { // write alias if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // id can be string or can have attribute write const aliasId = typeof obj.common.alias.id.write === 'string' ? obj.common.alias.id.write : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } // check the rights checkStates(aliasId, options, 'setState', (err, targetObj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(aliasId, tools.formatAliasValue(obj && obj.common, targetObj && targetObj.common, state, logger, this.namespaceLog), callback); } }); } else { logger.warn(`${this.namespaceLog} Alias ${id} has no target 4`); return tools.maybeCallbackWithError(callback, `Alias ${id} has no target`); } } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(id, state, callback); } } }); } else { // write alias if (id.startsWith(ALIAS_STARTS_WITH)) { if (!adapterObjects) { this.log.info('setForeignState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // read alias id adapterObjects.getObject(id, options, (err, obj) => { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // alias id can be a string or can have id.write const aliasId = typeof obj.common.alias.id.write === 'string' ? obj.common.alias.id.write : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } if (!adapterObjects) { // if objects is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // read object for formatting adapterObjects.getObject(aliasId, options, (err, targetObj) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(aliasId, tools.formatAliasValue(obj && obj.common, targetObj && targetObj.common, state, logger, this.namespaceLog), callback); }); } else { logger.warn(`${this.namespaceLog} ${err ? err.message : `Alias ${id} has no target 5`}`); return tools.maybeCallbackWithError(callback, err ? err.message : `Alias ${id} has no target`); } }); } else { if (this.performStrictObjectChecks) { if (!adapterObjects) { // if objects is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // validate that object exists, read-only logic ok, type ok, etc. won't throw now await this._performStrictObjectCheck(id, state); } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setState(id, state, callback); } } }; /** * Promise-version of Adapter.setForeignState */ this.setForeignStateAsync = tools.promisify(this.setForeignState, this); /** * Writes value into states DB for any instance, but only if state changed. * * This function can write values into states DB for all instances and system states too. * ack, options and callback are optional * * @alias setForeignStateChanged * @memberof Adapter * @param {string} id object ID of the state. * @param {object|string|number|boolean} state simple value or object with attribues. * If state is object and ack exists too as function argument, function argument has priority. * <pre><code> * { * val: value, * ack: true|false, // default - false; is command(false) or status(true) * ts: timestampMS, // default - now * q: qualityAsNumber, // default - 0 (ok) * from: origin, // default - this adapter * c: comment, // default - empty * expire: expireInSeconds // default - 0 * lc: timestampMS // default - automatic calculation * } * </code></pre> * @param {boolean} [ack] optional is command(false) or status(true) * @param {object} [options] optional user context * @param {ioBroker.SetStateChangedCallback} [callback] optional return error and id * <pre><code> * function (err, id) { * if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err); * } * </code></pre> */ this.setForeignStateChanged = (id, state, ack, options, callback) => { if (typeof state === 'object' && typeof ack !== 'boolean') { callback = options; options = ack; ack = undefined; } if (typeof options === 'function') { callback = options; options = {}; } if (typeof ack === 'function') { callback = ack; ack = undefined; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setForeignStateChanged not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } if (tools.isObject(state)) { // Verify that the passed state object is valid try { validateSetStateObjectArgument(state); } catch (e) { return tools.maybeCallbackWithError(callback, e); } } else { // wrap non-object values in a state object state = state !== undefined ? {val: state} : {}; } if (state.val === undefined && !Object.keys(state).length) { // undefined is not allowed as state.val -> return this.log.info(`undefined is not a valid state value for id "${id}"`); // TODO: reactivate line below + test in in next controller version (02.05.2021) // return tools.maybeCallbackWithError(callback, 'undefined is not a valid state value'); } if (ack !== undefined) { state.ack = ack; } // if state.from provided, we use it else, we set default property state.from = typeof state.from === 'string' && state.from !== '' ? state.from : `system.adapter.${this.namespace}`; state.user = (options ? options.user : '') || SYSTEM_ADMIN_USER; const mId = id.replace(FORBIDDEN_CHARS, '_'); if (mId !== id) { logger.warn(`${this.namespaceLog} Used invalid characters: ${id} changed to ${mId}`); id = mId; } if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'setState', err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { _setStateChangedHelper(id, state, callback); } }); } else { _setStateChangedHelper(id, state, callback); } }; /** * Promise-version of Adapter.setForeignStateChanged */ this.setForeignStateChangedAsync = tools.promisify(this.setForeignStateChanged, this); /** * Read value from states DB. * * This function can read values from states DB for this adapter. * Only Ids that belong to this adapter can be read. So the function automatically adds "adapter.X." to ID. * * @alias getState * @memberof Adapter * @param {string} id object ID of the state. * @param {object} options optional user context * @param {ioBroker.GetStateCallback} callback return result * <pre><code> * function (err, state) { * if (err) adapter.log.error('Cannot read value: ' + err); * } * </code></pre> * * See possible attributes of the state in @setState explanation */ this.getState = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!adapterObjects) { this.log.info('getState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } id = this._fixId(id, false); if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'getState', err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (id.startsWith(ALIAS_STARTS_WITH)) { adapterObjects.getObject(id, options, (err, obj) => { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // alias id can be string or can have attribute id.read const aliasId = typeof obj.common.alias.id.read === 'string' ? obj.common.alias.id.read : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } if (this.oStates && this.oStates[aliasId]) { adapterObjects.getObject(aliasId, (err, sourceObj) => { const state = deepClone(this.oStates[aliasId]); return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); }); } else { adapterObjects.getObject(aliasId, (err, sourceObj) => { adapterStates.getState(aliasId, (err, state) => { return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); }); }); } } else { logger.warn(`${this.namespaceLog} ${err ? err.message : `Alias ${id} has no target 6`}`); return tools.maybeCallbackWithError(callback, err ? err.message : `Alias ${id} has no target`); } }); } else { if (this.oStates && this.oStates[id]) { return tools.maybeCallbackWithError(callback, null, this.oStates[id]); } else { adapterStates.getState(id, callback); } } } }); } else { if (id.startsWith(ALIAS_STARTS_WITH)) { adapterObjects.getObject(id, options, (err, obj) => { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // alias id can be string or can have attribute id.read const aliasId = typeof obj.common.alias.id.read === 'string' ? obj.common.alias.id.read : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } adapterObjects.getObject(aliasId, options, (err, sourceObj) => { if (this.oStates && this.oStates[aliasId]) { const state = deepClone(this.oStates[aliasId]); return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); } else { adapterStates.getState(aliasId, (err, state) => { return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); }); } }); } else { logger.warn(`${this.namespaceLog} ${err ? err.message : `Alias ${id} has no target 7`}`); return tools.maybeCallbackWithError(callback, err ? err.message : `Alias ${id} has no target`); } }); } else { if (this.oStates && this.oStates[id]) { return tools.maybeCallbackWithError(callback, null, this.oStates[id]); } else { adapterStates.getState(id, callback); } } } }; /** * Promise-version of Adapter.getState */ this.getStateAsync = tools.promisify(this.getState, this); /** * Read value from states DB for any instance and system state. * * This function can read values from states DB for all instances and adapters. It expects the full path of object ID. * * @alias getForeignState * @memberof Adapter * @param {string} id object ID of the state. * @param {object} options optional user context * @param {ioBroker.GetStateCallback} callback return result * <pre><code> * function (err, state) { * if (err) adapter.log.error('Cannot read value: ' + err); * } * </code></pre> * * See possible attributes of the state in @setState explanation */ this.getForeignState = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!adapterObjects) { this.log.info('getForeignState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'getState', (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (id.startsWith(ALIAS_STARTS_WITH)) { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // id can be string or can have attribute id.read const aliasId = typeof obj.common.alias.id.read === 'string' ? obj.common.alias.id.read : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } if (aliasId) { if (this.oStates && this.oStates[aliasId]) { checkStates(aliasId, options, 'getState', (err, sourceObj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { const state = deepClone(this.oStates[aliasId]); return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); } }); } else { checkStates(aliasId, options, 'getState', (err, sourceObj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { this.inputCount++; adapterStates.getState(aliasId, (err, state) => tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog))); } }); } } } else { logger.warn(`${this.namespaceLog} Alias ${id} has no target 8`); return tools.maybeCallbackWithError(callback, `Alias ${id} has no target`); } } else { if (this.oStates && this.oStates[id]) { return tools.maybeCallbackWithError(callback, null, this.oStates[id]); } else { adapterStates.getState(id, callback); } } } }); } else { if (id.startsWith(ALIAS_STARTS_WITH)) { adapterObjects.getObject(id, (err, obj) => { if (obj && obj.common && obj.common.alias && obj.common.alias.id) { // id can be string or can have attribute id.read const aliasId = typeof obj.common.alias.id.read === 'string' ? obj.common.alias.id.read : obj.common.alias.id; // validate here because we use objects/states db directly try { validateId(aliasId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${id}: ${e.message}`); return tools.maybeCallbackWithError(callback, `Error validating alias id of ${id}: ${e.message}`); } adapterObjects.getObject(aliasId, (err, sourceObj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } if (this.oStates && this.oStates[aliasId]) { const state = deepClone(this.oStates[aliasId]); return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); } else { this.inputCount++; adapterStates.getState(aliasId, (err, state) => { return tools.maybeCallbackWithError(callback, err, tools.formatAliasValue(sourceObj && sourceObj.common, obj.common, state, logger, this.namespaceLog)); }); } }); } else { logger.warn(`${this.namespaceLog} ${err ? err.message : `Alias ${id} has no target 9`}`); return tools.maybeCallbackWithError(callback, err ? err.message : `Alias ${id} has no target`); } }); } else { if (this.oStates && this.oStates[id]) { return tools.maybeCallbackWithError(callback, null, this.oStates[id]); } else { this.inputCount++; adapterStates.getState(id, callback); } } } }; /** * Promise-version of Adapter.getForeignState */ this.getForeignStateAsync = tools.promisify(this.getForeignState, this); /** * Read historian data for states of any instance or system state. * * This function can read values from history adapters like: history, sql, influxdb. It expects the full path of object ID. * Normally only foreign history has interest, so there is no getHistory and getForeignHistory * * Possible options: * * - instance - (optional) name of instance, where to read the historian data, e.g. 'history.0', 'sql.1'. By default will be taken from system settings. * - start - (optional) time in ms - Date.now()', by default is (now - 1 week) * - end - (optional) time in ms - Date.now()', by default is (now + 5000 seconds) * - step - (optional) used in aggregate (m4, max, min, average, total) step in ms of intervals * - count - number of values if aggregate is 'onchange' or number of intervals if other aggregate method. Count will be ignored if step is set. * - from - if from field should be included in answer * - ack - if ack field should be included in answer * - q - if q field should be included in answer * - addId - if id field should be included in answer * - limit - do not return more entries than limit * - ignoreNull - if null values should be include (false), replaced by last not null value (true) or replaced with 0 (0) * - sessionId - (optional) identifier of request, will be returned back in the answer * - aggregate - aggregate method: * - minmax - used special algorithm. Splice the whole time range in small intervals and find for every interval max, min, start and end values. * - max - Splice the whole time range in small intervals and find for every interval max value and use it for this interval (nulls will be ignored). * - min - Same as max, but take minimal value. * - average - Same as max, but take average value. * - total - Same as max, but calculate total value. * - count - Same as max, but calculate number of values (nulls will be calculated). * - none - No aggregation at all. Only raw values in given period. * * @alias getHistory * @memberof Adapter * @param {string} id object ID of the state. * @param {object} options see function description * @param {ioBroker.GetHistoryCallback} callback return result * <pre><code> * function (error, result, step, sessionId) { * if (error) adapter.log.error('Cannot read value: ' + err); * } * </code></pre> * * See possible attributes of the state in @setState explanation */ this.getHistory = (id, options, callback) => { try { validateId(id, true, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } options = options || {}; options.end = options.end || Date.now() + 5000000; if (!options.count && !options.start) { options.start = options.start || Date.now() - 604800000; // - 1 week } if (!options.instance) { if (!this.defaultHistory) { // read default history instance from system.config return getDefaultHistory(() => this.getHistory(id, options, callback)); } else { options.instance = this.defaultHistory; } } this.sendTo(options.instance || 'history.0', 'getHistory', {id: id, options: options}, res => tools.maybeCallbackWithError(callback, res.error, res.result, res.step, res.sessionId)); }; /** * Promise-version of Adapter.getHistory */ this.getHistoryAsync = tools.promisify(this.getHistory, this, ['result', 'step', 'sessionId']); /** * Convert ID into object with device's, channel's and state's name. * * Convert "adapter.instance.D.C.S" in object {device: D, channel: C, state: S} * Convert ID to {device: D, channel: C, state: S} * * @alias idToDCS * @memberof Adapter * @param {string} id short or long string of ID like "stateID" or "adapterName.0.stateID". * @return {object} parsed ID as an object */ this.idToDCS = id => { if (!id) { return null; } const parts = id.split('.'); if (parts[0] + '.' + parts[1] !== this.namespace) { logger.warn(`${this.namespaceLog} Try to decode id not from this adapter`); return null; } return {device: parts[2], channel: parts[3], state: parts[4]}; }; /** * Deletes a state of this instance. * The object will NOT be deleted. If you want to delete it too, use @delObject instead. * * It is not required to provice the adapter namespace, because it will automatically be added. * E.g. to delete "adapterName.X.myObject", only "myObject" is required as ID. * * No error is returned if state does not exist. * * @alias delState * @memberof Adapter * @param {string} id exactly object ID (without namespace) * @param {object} [options] optional user context * @param {ioBroker.ErrorCallback} [callback] return result * <pre><code> * function (err) { * if (err) adapter.log.error('Cannot delete object: ' + err); * } * </code></pre> */ this.delState = (id, options, callback) => { try { validateId(id, false, null); } catch (err) { return tools.maybeCallbackWithError(callback, err); } // delState does the same as delForeignState, but fixes the ID first id = this._fixId(id); this.delForeignState(id, options, callback); }; /** * Promise-version of Adapter.delState */ this.delStateAsync = tools.promisify(this.delState, this); /** * Deletes a state of any adapter. * The object is NOT deleted. If you want to delete it too, use @delForeignObject instead. * * No error is returned if state does not exist. * * @alias delForeignState * @memberof Adapter * @param {string} id long string for ID like "adapterName.0.stateID". * @param {object} [options] optional argument to describe the user context * @param {ioBroker.ErrorCallback} [callback] return result function (err) {} */ this.delForeignState = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = null; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('delForeignState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'delState', err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { adapterStates.delState(id, callback); } }); } else { adapterStates.delState(id, callback); } }; /** * Promise-version of Adapter.delForeignState */ this.delForeignStateAsync = tools.promisify(this.delForeignState, this); /** * Read all states of this adapter, that pass the pattern * * Allows to read all states of current adapter according to pattern. To read all states of current adapter use: * <pre><code> * adapter.getStates('*', function (err, states) { * for (var id in states) { * adapter.log.debug('"' + id + '" = "' + states[id].val); * } * }); * </code></pre> * * @alias getStates * @memberof Adapter * @param {string} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. * @param {object} options optional argument to describe the user context * @param {ioBroker.GetStatesCallback} callback return result function (err, states) {}, where states is an object like {"ID1": {"val": 1, "ack": true}, "ID2": {"val": 2, "ack": false}, ...} */ this.getStates = (pattern, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } pattern = this._fixId(pattern, true); this.getForeignStates(pattern, options, callback); }; /** * Promise-version of Adapter.getStates */ this.getStatesAsync = tools.promisify(this.getStates, this); this._processStatesSecondary = function (keys, targetObjs, srcObjs, callback) { adapterStates.getStates(keys, (err, arr) => { if (err) { return tools.maybeCallbackWithError(callback, err); } const result = {}; for (let i = 0; i < keys.length; i++) { const obj = targetObjs && targetObjs[i]; if (typeof arr[i] === 'string') { try { arr[i] = JSON.parse(arr[i]); } catch { // if it is not binary state arr[i] < 2000 && logger.error(this.namespaceLog + ' Cannot parse state "' + keys[i] + ': ' + arr[i]); } } if (obj && obj.common && obj.common.alias) { if (obj.common.alias.val !== undefined) { result[obj._id] = {val: obj.common.alias.val, ts: Date.now(), q: 0}; } else if (srcObjs[i]) { result[obj._id] = tools.formatAliasValue(srcObjs[i].common, obj.common, arr[i] || null, logger, this.namespaceLog) || null; } else { result[obj._id || keys[i]] = arr[i] || null; } } else { result[(obj && obj._id) || keys[i]] = arr[i] || null; } } return tools.maybeCallbackWithError(callback, null, result); }); }; this._processStates = function (keys, targetObjs, callback) { let aliasFound; const aIds = keys.map(id => { if (typeof id === 'string' && id.startsWith(ALIAS_STARTS_WITH)) { aliasFound = true; return id; } else { return null; } }); // if any ID from aliases found if (aliasFound) { // make a copy of original array keys = [...keys]; // read aliases objects this._getObjectsByArray(aIds, targetObjs, options, (errors, targetObjs) => { const srcIds = []; // replace aliases ID with targets targetObjs.forEach((obj, i) => { if (obj && obj.common && obj.common.alias) { // alias id can be string or can have attribute read (this is used by getStates -> so read is important) const aliasId = obj.common.alias.id && typeof obj.common.alias.id.read === 'string' ? obj.common.alias.id.read : obj.common.alias.id; keys[i] = aliasId || null; srcIds[i] = keys[i]; } }); // srcObjs and targetObjs could be merged this._getObjectsByArray(srcIds, null, options, (errors, srcObjs) => this._processStatesSecondary(keys, targetObjs, srcObjs, callback)); }); } else { this._processStatesSecondary(keys, null, null, callback); } }; /** * Read all states of all adapters (and system states), that pass the pattern * * Allows to read all states of current adapter according to pattern. To read all states of current adapter use: * <pre><code> * adapter.getStates('*', function (err, states) { * for (var id in states) { * adapter.log.debug('"' + id + '" = "' + states[id].val); * } * }); * </code></pre> * * @alias getForeignStates * @memberof Adapter * @param {string | string[]} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. * @param {object} options optional argument to describe the user context * @param {ioBroker.GetStatesCallback} callback return result function (err, states) {}, where states is an object like {"ID1": {"val": 1, "ack": true}, "ID2": {"val": 2, "ack": false}, ...} */ this.getForeignStates = (pattern, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } if (typeof pattern === 'function') { callback = pattern; pattern = '*'; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getForeignStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!adapterObjects) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getForeignStates not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (pattern instanceof RegExp) { logger.error(`${this.namespaceLog} Regexp is not supported for "getForeignStates"`); return tools.maybeCallbackWithError(callback, 'Regexp is not supported for "getForeignStates"'); } if (!Array.isArray(pattern) && typeof pattern !== 'string') { logger.error(`${this.namespaceLog} The Pattern for "getForeignStates" needs to be an Array or an String. ${typeof pattern} provided.`); return tools.maybeCallbackWithError(callback, `The Pattern for "getForeignStates" needs to be an Array or an String. ${typeof pattern} provided.`); } // if pattern is array if (Array.isArray(pattern)) { if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(pattern, options, 'getState', (err, keys, objs) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { this._processStates(keys, objs, callback); } }); } else { this._processStates(pattern, options && options._objects, callback); } } else { // read first the keys for pattern let params = {}; if (pattern && pattern !== '*') { params = { startkey: pattern.replace(/\*/g, ''), endkey: pattern.replace(/\*/g, '\u9999') }; } let originalChecked = undefined; if (options.checked !== undefined) { originalChecked = options.checked; } options.checked = true; // in special maintenance mode, just returns all states. Aliases are not supported in this mode if (options.user === SYSTEM_ADMIN_USER && options.maintenance) { adapterStates.getKeys(pattern, (err, keys) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { this._processStatesSecondary(keys, null, null, callback); } }); } adapterObjects.getObjectView('system', 'state', params, options, (err, res) => { if (originalChecked !== undefined) { options.checked = originalChecked; } else { options.checked = undefined; } if (err) { return tools.maybeCallbackWithError(callback, err); } if (!res || !res.rows) { return tools.maybeCallbackWithError(callback, null, {}); } const keys = []; const objs = []; // filter out let regEx; // process patterns like "*.someValue". The patterns "someValue.*" will be processed by getObjectView if (pattern !== '*' && pattern[pattern.length - 1] !== '*') { regEx = new RegExp(tools.pattern2RegEx(pattern)); } for (let i = 0; i < res.rows.length; i++) { const id = res.rows[i].id; if (id && (!regEx || regEx.test(id))) { keys.push(id); objs.push(res.rows[i].value); } } options._objects = objs; this.getForeignStates(keys, options, callback); }); } }; /** * Promise-version of Adapter.getForeignStates */ this.getForeignStatesAsync = tools.promisify(this.getForeignStates, this); this._addAliasSubscribe = (aliasObj, pattern, callback) => { if (aliasObj && aliasObj.common && aliasObj.common.alias && aliasObj.common.alias.id) { if (aliasObj.type !== 'state') { logger.warn(`${this.namespaceLog} Expected alias ${aliasObj._id} to be of type "state", got "${aliasObj.type}"`); return tools.maybeCallbackWithError(callback, new Error(`Expected alias ${aliasObj._id} to be of type "state", got "${aliasObj.type}"`)); } // id can be string or can have attribute read const sourceId = typeof aliasObj.common.alias.id.read === 'string' ? aliasObj.common.alias.id.read : aliasObj.common.alias.id; // validate here because we use objects/states db directly try { validateId(sourceId, true, null); } catch (e) { logger.warn(`${this.namespaceLog} Error validating alias id of ${aliasObj._id}: ${e.message}`); return tools.maybeCallbackWithError(callback, new Error(`Error validating alias id of ${aliasObj._id}: ${e.message}`)); } this.aliases[sourceId] = this.aliases[sourceId] || {source: null, targets: []}; const targetEntry = { alias: JSON.parse(JSON.stringify(aliasObj.common.alias)), id: aliasObj._id, pattern, type: aliasObj.common.type, max: aliasObj.common.max, min: aliasObj.common.min, unit: aliasObj.common.unit }; this.aliases[sourceId].targets.push(targetEntry); if (!this.aliases[sourceId].source) { adapterStates.subscribe(sourceId, () => adapterObjects.getObject(sourceId, options, (err, sourceObj) => { if (sourceObj && sourceObj.common) { if (!this.aliases[sourceObj._id]) { logger.error(`${this.namespaceLog} Alias subscription error. Please check your alias definitions: sourceId=${sourceId}, sourceObj=${JSON.stringify(sourceObj)}`); } else { this.aliases[sourceObj._id].source = {}; this.aliases[sourceObj._id].source.min = sourceObj.common.min; this.aliases[sourceObj._id].source.max = sourceObj.common.max; this.aliases[sourceObj._id].source.type = sourceObj.common.type; this.aliases[sourceObj._id].source.unit = sourceObj.common.unit; } } return tools.maybeCallbackWithError(callback, err); })); } else { return tools.maybeCallback(callback); } } else if (aliasObj && aliasObj.type === 'state') { // if state and no id given -> if no state just ignore it logger.warn(`${this.namespaceLog} Alias ${aliasObj._id} has no target 12`); return tools.maybeCallbackWithError(callback, new Error(`Alias ${aliasObj._id} has no target 12`)); } else { return tools.maybeCallback(callback); } }; this._removeAliasSubscribe = (sourceId, aliasObj, pattern, callback) => { if (typeof pattern === 'function') { callback = pattern; pattern = null; } if (!this.aliases[sourceId]) { return tools.maybeCallback(callback); } // remove from targets array const pos = typeof aliasObj === 'number' ? aliasObj : this.aliases[sourceId].targets.indexOf(aliasObj); if (pos !== -1) { this.aliases[sourceId].targets.splice(pos, 1); // unsubscribe if no more aliases exists if (!this.aliases[sourceId].targets.length) { delete this.aliases[sourceId]; return adapterStates.unsubscribe(sourceId, callback); } } return tools.maybeCallback(callback); }; /** * Subscribe for changes on all states of all adapters (and system states), that pass the pattern * * Allows to Subscribe on changes all states of all instances according to pattern. E.g. to read all states of 'adapterName.X' instance use: * <pre><code> * adapter.subscribeForeignStates('adapterName.X.*'); * </code></pre> * * @alias subscribeForeignStates * @memberof Adapter * @param {string | string[] | RegExp} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. * @param {object} [options] optional argument to describe the user context * @param {ioBroker.ErrorCallback} [callback] return result function (err) {} */ this.subscribeForeignStates = async (pattern, options, callback) => { pattern = pattern || '*'; if (typeof options === 'function') { callback = options; options = null; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('subscribeForeignStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // Todo check rights for options await autoSubscribeOn(); if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('subscribeForeignStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!adapterObjects) { this.log.info('subscribeForeignStates not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // compare if this pattern for one of auto-subscribe adapters for (const autoSubEntry of this.autoSubscribe) { if (typeof pattern === 'string' && (pattern === '*' || pattern.startsWith(`${autoSubEntry}.`))) { // put this pattern into adapter list adapterStates.getState(`system.adapter.${autoSubEntry}.subscribes`, (err, state) => { state = {}; state.val = state.val || '{}'; let subs; try { subs = JSON.parse(state.val); } catch { logger.error(`${this.namespaceLog} Cannot parse subscribes for "${autoSubEntry}.subscribes"`); } subs[pattern] = subs[pattern] || {}; subs[pattern][this.namespace] = subs[pattern][this.namespace] || 0; subs[pattern][this.namespace]++; this.outputCount++; adapterStates.setState(`system.adapter.${autoSubEntry}.subscribes`, JSON.stringify(subs)); }); } } // RegExp is allowed for alias only if (pattern instanceof RegExp && pattern.toString().indexOf('alias') !== -1) { logger.error(`${this.namespaceLog} Regexp is not supported for "subscribeForeignStates"`); return tools.maybeCallbackWithError(callback, 'Regexp is not supported for "subscribeForeignStates"'); } if (Array.isArray(pattern)) { // get all aliases const aliasesIds = pattern .map(id => typeof id === 'string' && id.startsWith(ALIAS_STARTS_WITH) ? id : null) .filter(id => id); // get all non aliases const nonAliasesIds = pattern .map(id => typeof id === 'string' && !id.startsWith(ALIAS_STARTS_WITH) ? id : null) .filter(id => id); const promises = []; for (let aliasPattern of pattern) { aliasPattern = aliasPattern instanceof RegExp ? JSON.stringify(aliasPattern) : aliasPattern; if (typeof aliasPattern === 'string' && (aliasPattern.startsWith(ALIAS_STARTS_WITH) || aliasPattern.includes('*')) && !this.aliasPatterns.includes(aliasPattern)) { // its a new alias conform pattern to store this.aliasPatterns.push(aliasPattern); } } if (aliasesIds.length) { if (!this._aliasObjectsSubscribed) { this._aliasObjectsSubscribed = true; adapterObjects.subscribe(`${ALIAS_STARTS_WITH}*`); } const aliasObjs = await new Promise(resolve => this._getObjectsByArray(aliasesIds, null, options, (errors, aliasObjs) => resolve(aliasObjs))); aliasObjs.forEach(aliasObj => promises.push(new Promise(resolve => this._addAliasSubscribe(aliasObj, aliasObj._id, resolve)))); } if (nonAliasesIds.length) { nonAliasesIds.forEach(id => promises.push(new Promise(resolve => adapterStates.subscribeUser(id, resolve)))); } try { await Promise.all(promises); } catch (e) { logger.error(`${this.namespaceLog} Error on initial states subscription: ${e.message}`); } return tools.maybeCallback(callback); } else if ((typeof pattern === 'string' && pattern.includes('*')) || pattern instanceof RegExp) { if (typeof pattern === 'string' && (pattern === '*' || pattern.startsWith(ALIAS_STARTS_WITH)) || pattern instanceof RegExp) { if (!this._aliasObjectsSubscribed) { this._aliasObjectsSubscribed = true; adapterObjects.subscribe(`${ALIAS_STARTS_WITH}*`); } // read all aliases try { const objs = await this.getForeignObjectsAsync(pattern, null, null, options); const promises = []; const aliasPattern = pattern instanceof RegExp ? JSON.stringify(pattern) : pattern; if (!this.aliasPatterns.includes(aliasPattern)) { // its a new pattern to store this.aliasPatterns.push(aliasPattern); } Object.keys(objs).forEach(id => { // If alias if (id.startsWith(ALIAS_STARTS_WITH)) { const aliasObj = objs[id]; promises.push(new Promise(resolve => this._addAliasSubscribe(aliasObj, aliasPattern, resolve))); } }); try { await Promise.all(promises); } catch (e) { logger.error(`${this.namespaceLog} Error on initial states subscription: ${e.message}`); } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('subscribeForeignStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (promises.length && pattern !== '*') { return tools.maybeCallback(callback); } else { // no alias objects found or pattern * adapterStates.subscribeUser(pattern, callback); } } catch (e) { logger.warn(`${this.namespaceLog} Cannot subscribe to ${pattern}: ${e.message}`); return tools.maybeCallbackWithError(callback, e); } } else { adapterStates.subscribeUser(pattern, callback); } } else if (typeof pattern === 'string' && pattern.startsWith(ALIAS_STARTS_WITH)) { if (!this._aliasObjectsSubscribed) { this._aliasObjectsSubscribed = true; adapterObjects.subscribe(`${ALIAS_STARTS_WITH}*`); } // aliases['sourceId'] = { // source: {common attributes}, // targets: [ // { // alias: {}, // id: 'aliasId', // pattern: 'some pattern', // type: stateType, // max: number, // min: number, // } // ] // }; // just read one alias Object try { const aliasObj = await adapterObjects.getObjectAsync(pattern, options); if (aliasObj) { // cb will be called, but await for catching promisified part await this._addAliasSubscribe(aliasObj, pattern, callback); } else { return tools.maybeCallback(callback); } } catch (e) { logger.warn(`${this.namespaceLog} cannot subscribe on alias "${pattern}": ${e.message}`); } } else { adapterStates.subscribeUser(pattern, callback); } }; /** * Promise-version of Adapter.subscribeForeignStates */ this.subscribeForeignStatesAsync = tools.promisify(this.subscribeForeignStates, this); /** * Unsubscribe for changes for given pattern * * This function allows to unsubscribe from changes. The pattern must be equal to requested one. * * <pre><code> * adapter.subscribeForeignStates('adapterName.X.*'); * adapter.unsubscribeForeignStates('adapterName.X.abc*'); // This will not work * adapter.unsubscribeForeignStates('adapterName.X.*'); // Valid unsubscribe * </code></pre> * * @alias unsubscribeForeignStates * @memberof Adapter * @param {string | string[] | RegExp} pattern string in form 'adapter.0.*'. Must be the same as subscribe. * @param {object} [options] optional argument to describe the user context * @param {ioBroker.ErrorCallback} [callback] return result function (err) {} */ this.unsubscribeForeignStates = (pattern, options, callback) => { pattern = pattern || '*'; if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('unsubscrubeForeignStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // Todo check rights for options if (typeof options === 'function') { callback = options; options = null; } if (this.autoSubscribe && typeof pattern === 'string') { for (let s = 0; s < this.autoSubscribe.length; s++) { if (pattern === '*' || pattern.substring(0, this.autoSubscribe[s].length + 1) === `${this.autoSubscribe[s]}.`) { // remove this pattern from adapter list adapterStates.getState(`system.adapter.${this.autoSubscribe[s]}.subscribes`, (err, state) => { if (!state || !state.val) { return; } let subs; try { subs = JSON.parse(state.val); } catch { logger.error(`${this.namespaceLog} Cannot parse subscribes for "${this.autoSubscribe[s]}.subscribes"`); return; } if (!subs[pattern]) { return; } if (subs[pattern][this.namespace] === undefined) { return; } subs[pattern][this.namespace]--; if (subs[pattern][this.namespace] <= 0) { delete subs[pattern][this.namespace]; } // if no other subs are there if (!Object.keys(subs[pattern]).length) { delete subs[pattern]; } this.outputCount++; adapterStates.setState(`system.adapter.${this.autoSubscribe[s]}.subscribes`, subs); }); } } } const promises = []; let aliasPattern; if (Array.isArray(pattern)) { // process every entry as single unsubscribe pattern.forEach(_pattern => promises.push(new Promise(resolve => this.unsubscribeForeignStates(_pattern, resolve)))); } else if (pattern instanceof RegExp) { aliasPattern = JSON.stringify(pattern); // check all aliases promises.push(new Promise(resolve => adapterStates.unsubscribeUser(pattern, resolve))); } else if (typeof pattern === 'string' && (pattern.includes('*') || pattern.startsWith(ALIAS_STARTS_WITH))) { if (pattern === '*' || pattern.startsWith(ALIAS_STARTS_WITH)) { aliasPattern = pattern; // check all aliases if (pattern === '*') { promises.push(new Promise(resolve => adapterStates.unsubscribeUser(pattern, resolve))); } } else { promises.push(new Promise(resolve => adapterStates.unsubscribeUser(pattern, resolve))); } } else { promises.push(new Promise(resolve => adapterStates.unsubscribeUser(pattern, resolve))); } // if pattern known, remove it from alias patterns to not subscribe to further matching aliases this.aliasPatterns = this.aliasPatterns.filter(pattern => pattern !== aliasPattern); if (aliasPattern) { Object.keys(this.aliases).forEach(sourceId => { for (let i = this.aliases[sourceId].targets.length - 1; i >= 0; i--) { if (this.aliases[sourceId].targets[i].pattern === aliasPattern) { promises.push(new Promise(resolve => this._removeAliasSubscribe(sourceId, i, resolve))); } } }); } Promise.all(promises).then(() => { // if no alias subscribed any longer, remove subscription if (!Object.keys(this.aliases).length && this._aliasObjectsSubscribed) { this._aliasObjectsSubscribed = false; adapterObjects.unsubscribe(`${ALIAS_STARTS_WITH}*`); } return tools.maybeCallback(callback); }); }; /** * Promise-version of Adapter.unsubscribeForeignStates */ this.unsubscribeForeignStatesAsync = tools.promisify(this.unsubscribeForeignStates, this); /** * Subscribe for changes on all states of this instance, that pass the pattern * * Allows to Subscribe on changes all states of current adapter according to pattern. To read all states of current adapter use: * <pre><code> * adapter.subscribeStates('*'); // subscribe for all states of this adapter * </code></pre> * * @alias subscribeStates * @memberof Adapter * @param {string} pattern string in form 'adapter.0.*' or like this. Only string allowed * @param {object} [options] optional argument to describe the user context * @param {ioBroker.ErrorCallback} [callback] */ this.subscribeStates = (pattern, options, callback) => { // Todo check rights for options if (typeof options === 'function') { callback = options; options = null; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('subscribeStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // Exception. Threat the '*' case automatically if (!pattern || pattern === '*') { adapterStates.subscribeUser(this.namespace + '.*', callback); } else { pattern = this._fixId(pattern, true); adapterStates.subscribeUser(pattern, callback); } }; /** * Promise-version of Adapter.subscribeStates */ this.subscribeStatesAsync = tools.promisify(this.subscribeStates, this); /** * Unsubscribe for changes for given pattern for own states. * * This function allows to unsubscribe from changes. The pattern must be equal to requested one. * * <pre><code> * adapter.subscribeForeignStates('*'); * adapter.unsubscribeForeignStates('abc*'); // This will not work * adapter.unsubscribeForeignStates('*'); // Valid unsubscribe * </code></pre> * * @alias unsubscribeStates * @memberof Adapter * @param {string} pattern string in form 'adapter.0.*'. Must be the same as subscribe. * @param {object} [options] optional argument to describe the user context * @param {ioBroker.ErrorCallback} [callback] */ this.unsubscribeStates = (pattern, options, callback) => { // Todo check rights for options if (typeof options === 'function') { callback = options; options = null; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('unsubscribeStates not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } if (!pattern || pattern === '*') { adapterStates.unsubscribeUser(this.namespace + '.*', callback); } else { pattern = this._fixId(pattern, true); adapterStates.unsubscribeUser(pattern, callback); } }; /** * Promise-version of Adapter.unsubscribeStates */ this.unsubscribeStatesAsync = tools.promisify(this.unsubscribeStates, this); /** * Decrypt the password/value with given key * @param {string} secretVal to use for decrypt (or value if only one parameter is given) * @param {string} [value] value to decrypt (if secret is provided) * @returns {string} */ this.decrypt = (secretVal, value) => { if (value === undefined) { value = secretVal; secretVal = systemSecret; } return tools.decrypt(secretVal, value); }; /** * Encrypt the password/value with given key * @param {string} secretVal to use for encrypt (or value if only one parameter is given) * @param {string} [value] value to encrypt (if secret is provided) * @returns {string} */ this.encrypt = (secretVal, value) => { if (value === undefined) { value = secretVal; secretVal = systemSecret; } return tools.encrypt(secretVal, value); }; this.pushFifo = (id, state, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('pushFifo not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.pushFifo(id, state, callback); }; this.trimFifo = (id, start, end, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('trimFifo not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.trimFifo(id, start, end, callback); }; this.getFifoRange = (id, start, end, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getFifoRange not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.getFifoRange(id, start, end, callback); }; this.getFifo = (id, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getFifo not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.getFifo(id, callback); }; this.lenFifo = (id, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('lenFifo not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.lenFifo(id, callback); }; this.subscribeFifo = (pattern, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('subscribeFifo not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.subscribeFifo(pattern); }; this.getSession = (id, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getSession not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.getSession(id, callback); }; this.setSession = (id, ttl, data, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setSession not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.setSession(id, ttl, data, callback); }; this.destroySession = (id, callback) => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('destroySession not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterStates.destroySession(id, callback); }; /** * Write binary block into redis, e.g image * * @alias setBinaryState * @memberof Adapter * * @param {string} id of state * @param {Buffer} binary data * @param {object} [options] optional * @param {ioBroker.ErrorCallback} [callback] * */ this.setBinaryState = (id, binary, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setBinaryState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // we need at least user or group for checkStates - if no given assume admin if (!options || !options.user) { options = options || {}; options.user = SYSTEM_ADMIN_USER; } // always read according object to set the binary flag checkStates(id, options, 'setState', (err, obj) => { if (!err && !obj) { return tools.maybeCallbackWithError(callback, 'Object does not exist'); } else if (!err && !obj.binary) { obj.binary = true; if (!adapterObjects) { this.log.info('setBinaryState not processed because Objects database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } adapterObjects.setObject(id, obj, err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setBinaryState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setBinaryState(id, binary, callback); } }); } else if (err) { return tools.maybeCallbackWithError(callback, err); } else { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('setBinaryState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } this.outputCount++; adapterStates.setBinaryState(id, binary, callback); } }); }; /** * Promise-version of Adapter.setBinaryState * * @alias setBinaryStateAsync * @memberof Adapter * @param {string} id of state * @param {Buffer} binary data * @param {object} [options] optional * @return promise * */ this.setBinaryStateAsync = tools.promisify(this.setBinaryState, this); // Read binary block from redis, e.g. image /** * Read a binary block from redis, e.g. an image * * @param {string} id The state ID * @param {object} options optional * @param {ioBroker.GetBinaryStateCallback} callback */ this.getBinaryState = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('getBinaryState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } // we need at least user or group for checkStates - if no given assume admin if (!options || !options.user) { options = options || {}; options.user = SYSTEM_ADMIN_USER; } // always read according object to set the binary flag checkStates(id, options, 'getState', (err, obj) => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { adapterStates.getBinaryState(id, (err, data) => { if (!err && data && obj && !obj.binary) { obj.binary = true; adapterObjects.setObject(id, obj, err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { return tools.maybeCallbackWithError(callback, null, data); } }); } else { // if no buffer, and state marked as not binary if (!err && !data && obj && !obj.binary) { return tools.maybeCallbackWithError(callback, 'State is not binary'); } else { return tools.maybeCallbackWithError(callback, err, data); } } }); } }); }; /** * Promise-version of Adapter.getBinaryState * * @alias getBinaryStateAsync * @memberof Adapter * */ this.getBinaryStateAsync = tools.promisify(this.getBinaryState, this); /** * Deletes binary state * * @alias delBinaryState * @memberof Adapter * * @param {string} id * @param {object} [options] * @param {ioBroker.ErrorCallback} [callback] * */ this.delBinaryState = (id, options, callback) => { if (typeof options === 'function') { callback = options; options = {}; } if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('delBinaryState not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } try { validateId(id, true, options); } catch (err) { return tools.maybeCallbackWithError(callback, err); } if (options && options.user && options.user !== SYSTEM_ADMIN_USER) { checkStates(id, options, 'delState', err => { if (err) { return tools.maybeCallbackWithError(callback, err); } else { adapterStates.delBinaryState(id, callback); } }); } else { adapterStates.delBinaryState(id, callback); } }; /** * Promise-version of Adapter.delBinaryState * * @alias delBinaryStateAsync * @memberof Adapter * @param {string} id * @param {object} [options] * @return promise * */ this.delBinaryStateAsync = tools.promisify(this.delBinaryState, this); /** * Return plugin instance * * @param name {string} name of the plugin to return * @returns {object} plugin instance or null if not existent or not isActive */ this.getPluginInstance = name => { if (!this.pluginHandler) { return null; } return this.pluginHandler.getPluginInstance(name); }; /** * Return plugin configuration * * @param name {string} name of the plugin to return * @returns {object} plugin configuration or null if not existent or not isActive */ this.getPluginConfig = name => { if (!this.pluginHandler) { return null; } return this.pluginHandler.getPluginConfig(name); }; }; // read all logs prepared for this adapter at start const readLogs = callback => { if (!adapterStates) { // if states is no longer existing, we do not need to unsubscribe this.log.info('readLogs not processed because States database not connected'); return tools.maybeCallbackWithError(callback, tools.ERRORS.ERROR_DB_CLOSED); } // read all stored messages adapterStates.getLog('system.adapter.' + this.namespace, (err, msg) => { if (msg) { this.emit('log', msg); setImmediate(() => readLogs(callback)); } else { return tools.maybeCallback(callback); } }); }; // debug function to find error with stop logging const checkLogging = () => { let logs = []; // LogList logs.push('Actual Loglist - ' + JSON.stringify(this.logList)); if (!adapterStates) { // if adapterState was destroyed, we can not continue return; } // Read current state of all log subscribers adapterStates.getKeys('*.logging', (err, keys) => { if (keys && keys.length) { if (!adapterStates) { // if adapterState was destroyed, we can not continue return; } adapterStates.getStates(keys, (err, obj) => { if (obj) { for (let i = 0; i < keys.length; i++) { // We can JSON.parse, but index is 16x faster if (obj[i]) { const id = keys[i].substring(0, keys[i].length - '.logging'.length); if ((typeof obj[i] === 'string' && (obj[i].indexOf('"val":true') !== -1 || obj[i].indexOf('"val":"true"') !== -1)) || (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true'))) { logs.push('Subscriber - ' + id + ' ENABLED'); } else { if (logs) { logs.push('Subscriber - ' + id + ' (disabled)'); } else { logger.error(this.namespaceLog + ' LOGINFO: Subscriber - ' + id + ' (disabled)'); } } } } } if (logs) { for (let m = 0; m < logs.length; m++) { logger.error(this.namespaceLog + ' LOGINFO: ' + logs[m]); } logs = null; } }); } }); }; const initLogging = callback => { // temporary log buffer let messages = []; // Read current state of all log subscriber if (!adapterStates) { // if adapterState was destroyed, we can not continue return; } adapterStates.getKeys('*.logging', (err, keys) => { if (keys && keys.length) { if (!adapterStates) { // if adapterState was destroyed, we can not continue return; } adapterStates.getStates(keys, (err, obj) => { if (obj) { for (let i = 0; i < keys.length; i++) { // We can JSON.parse, but index is 16x faster if (!obj[i]) { continue; } const id = keys[i].substring(0, keys[i].length - '.logging'.length); if (typeof obj[i] === 'string' && (obj[i].indexOf('"val":true') !== -1 || obj[i].indexOf('"val":"true"') !== -1)) { this.logRedirect(true, id); } else if (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true')) { this.logRedirect(true, id); } } if (this.logList.length && messages && messages.length && adapterStates && adapterStates.pushLog) { for (let m = 0; m < messages.length; m++) { for (let k = 0; k < this.logList.length; k++) { adapterStates.pushLog(this.logList[k], messages[m]); } } } } // clear log buffer messages = null; }); } else { // disable log buffer messages = null; } return tools.maybeCallback(callback); }); this.logRedirect = (isActive, id) => { // ignore itself if (id === 'system.adapter.' + this.namespace) { return; } if (isActive) { if (this.logList.indexOf(id) === -1) { this.logList.push(id); } } else { const pos = this.logList.indexOf(id); if (pos !== -1) { this.logList.splice(pos, 1); } } }; // If some message from logger // find our notifier transport const ts = logger.transports.find(t => t.name === 'NT'); ts.on('logged', info => { info.from = this.namespace; // emit to itself if (options.logTransporter && this.logRequired && !stopInProgress) { this.emit('log', info); } if (!this.logList.length) { // if log buffer still active if (messages && !options.logTransporter) { messages.push(info); // do not let messages to grow without limit if (messages.length > config.states.maxQueue) { messages.splice(0, messages.length - config.states.maxQueue); } } } else if (adapterStates && adapterStates.pushLog) { // Send to all adapter, that required logs for (let i = 0; i < this.logList.length; i++) { adapterStates.pushLog(this.logList[i], info); } } }); options.logTransporter = options.logTransporter || this.ioPack.common.logTransporter; if (options.logTransporter) { this.requireLog = isActive => { if (adapterStates) { if (this.logRequired !== isActive) { this.logRequired = isActive; // remember state if (!isActive) { if (this.logOffTimer) { clearTimeout(this.logOffTimer); } // disable log receiving after 10 seconds this.logOffTimer = setTimeout(() => { this.logOffTimer = null; logger.debug(this.namespaceLog + ' Change log subscriber state: FALSE'); this.outputCount++; adapterStates.setState('system.adapter.' + this.namespace + '.logging', {val: false, ack: true, from: 'system.adapter.' + this.namespace}); }, 10000); } else { if (this.logOffTimer) { clearTimeout(this.logOffTimer); this.logOffTimer = null; } else { logger.debug(this.namespaceLog + ' Change log subscriber state: true'); this.outputCount++; adapterStates.setState('system.adapter.' + this.namespace + '.logging', {val: true, ack: true, from: 'system.adapter.' + this.namespace}); } } } } }; this.processLog = msg => { msg && !stopInProgress && this.emit('log', msg); }; readLogs(); adapterStates.subscribeLog('system.adapter.' + this.namespace); } else { this.requireLog = _isActive => { logger.warn(this.namespaceLog + ' requireLog is not supported by this adapter! Please set common.logTransporter to true'); }; } }; const initAdapter = adapterConfig => { initLogging(() => { this.pluginHandler.setDatabaseForPlugins(adapterObjects, adapterStates); this.pluginHandler.initPlugins(adapterConfig, () => { if (!adapterStates) { // if adapterState was destroyed,we should not continue return; } adapterStates.subscribe('system.adapter.' + this.namespace + '.plugins.*'); if (options.instance === undefined) { if (!adapterConfig || !adapterConfig.common || !adapterConfig.common.enabled) { if (adapterConfig && adapterConfig.common && adapterConfig.common.enabled !== undefined) { !config.isInstall && logger.error(this.namespaceLog + ' adapter disabled'); } else { !config.isInstall && logger.error(this.namespaceLog + ' no config found for adapter'); } if (!config.isInstall && (!process.argv || !config.forceIfDisabled)) { const id = 'system.adapter.' + this.namespace; this.outputCount += 2; adapterStates.setState(id + '.alive', {val: true, ack: true, expire: 30, from: id}); let done = false; adapterStates.setState(id + '.connected', {val: true, ack: true, expire: 30, from: id}, () => { if (!done) { done = true; this.terminate(EXIT_CODES.NO_ADAPTER_CONFIG_FOUND); } }); setTimeout(() => { if (!done) { done = true; this.terminate(EXIT_CODES.NO_ADAPTER_CONFIG_FOUND); } }, 1000); return; } } if (!config.isInstall && !adapterConfig._id) { logger.error(this.namespaceLog + ' invalid config: no _id found'); this.terminate(EXIT_CODES.INVALID_ADAPTER_ID); return; } let name; let instance; if (!config.isInstall) { const tmp = adapterConfig._id.match(/^system\.adapter\.([a-zA-Z0-9-_]+)\.([0-9]+)$/); if (!tmp) { logger.error(this.namespaceLog + ' invalid config'); this.terminate(EXIT_CODES.INVALID_ADAPTER_ID); return; } name = tmp[1]; instance = parseInt(tmp[2]) || 0; } else { name = options.name; instance = 0; adapterConfig = adapterConfig || {common: {mode: 'once', name: name, protectedNative: []}, native: {}}; } if (adapterConfig.common.loglevel && !this.overwriteLogLevel) { // set configured in DB log level for (const trans of Object.keys(logger.transports)) { logger.transports[trans].level = adapterConfig.common.loglevel; } config.log.level = adapterConfig.common.loglevel; } this.name = adapterConfig.common.name; this.instance = instance; this.namespace = name + '.' + instance; this.namespaceLog = this.namespace + (this.startedInCompactMode ? ' (COMPACT)' : ' (' + process.pid + ')'); if (!this.startedInCompactMode) { process.title = 'io.' + this.namespace; } this.config = adapterConfig.native; this.host = adapterConfig.common.host; this.common = adapterConfig.common; if (adapterConfig.common.mode === 'subscribe' || adapterConfig.common.mode === 'schedule' || adapterConfig.common.mode === 'once') { this.stop = () => stop(true); } else if (this.startedInCompactMode) { this.stop = () => stop(false); this.kill = this.stop; } else { this.stop = () => stop(false); } // Monitor logging state adapterStates.subscribe('*.logging'); if (typeof options.message === 'function' && !adapterConfig.common.messagebox) { logger.error(this.namespaceLog + ' : message handler implemented, but messagebox not enabled. Define common.messagebox in io-package.json for adapter or delete message handler.'); } else if (/*typeof options.message === 'function' && */adapterConfig.common.messagebox) { this.mboxSubscribed = true; adapterStates.subscribeMessage('system.adapter.' + this.namespace); } } else { this.name = adapterConfig.name || options.name; this.instance = adapterConfig.instance || 0; this.namespace = this.name + '.' + this.instance; this.namespaceLog = this.namespace + (this.startedInCompactMode ? ' (COMPACT)' : ' (' + process.pid + ')'); this.config = adapterConfig.native || {}; this.common = adapterConfig.common || {}; this.host = this.common.host || tools.getHostName() || os.hostname(); } this.adapterConfig = adapterConfig; // Decrypt all attributes of encryptedNative const promises = []; if (Array.isArray(adapterConfig.encryptedNative)) { for (const attr of adapterConfig.encryptedNative) { // we can only decrypt strings if (typeof this.config[attr] === 'string') { promises.push(this.getEncryptedConfig(attr) .then(decryptedValue => this.config[attr] = decryptedValue) .catch(e => logger.error(this.namespaceLog + ' Can not decrypt attribute ' + attr + ': ' + e)) ); } } } else { // remove encrypted native from supported features, otherwise this can cause issues, if no adapter upload done with js-c v3+ yet const idx = supportedFeatures.indexOf('ADAPTER_AUTO_DECRYPT_NATIVE'); if (idx !== -1) { supportedFeatures.splice(idx, 1); } } let promiseReadSecret; // read the systemSecret if (systemSecret !== null) { promiseReadSecret = Promise.resolve(); } else { promiseReadSecret = new Promise(resolve => { this.getForeignObject('system.config', null, (err, data) => { if (data && data.native) { systemSecret = data.native.secret; } systemSecret = systemSecret || DEFAULT_SECRET; resolve(); }); }); } // Wait till secret read and all attributes decrypted promiseReadSecret .then(() => Promise.all(promises)) .then(() => { this.log = new Log(this.namespaceLog, config.log.level, logger); if (!adapterStates) { // if adapterState was destroyed,we should not continue return; } this.outputCount++; // set current loglevel adapterStates.setState('system.adapter.' + this.namespace + '.logLevel', { val: config.log.level, ack: true, from: 'system.adapter.' + this.namespace }); if (options.instance === undefined) { this.version = (this.pack && this.pack.version) ? this.pack.version : ((this.ioPack && this.ioPack.common) ? this.ioPack.common.version : 'unknown'); logger.info(`${this.namespaceLog} starting. Version ${this.version} in ${this.adapterDir}, node: ${process.version}, js-controller: ${controllerVersion}`); config.system = config.system || {}; config.system.statisticsInterval = parseInt(config.system.statisticsInterval, 10) || 15000; if (!config.isInstall) { reportInterval = setInterval(reportStatus, config.system.statisticsInterval); reportStatus(); const id = 'system.adapter.' + this.namespace; adapterStates.setState(id + '.compactMode', { ack: true, from: id, val: !!this.startedInCompactMode }); this.outputCount++; if (this.startedInCompactMode) { adapterStates.setState(id + '.cpu', {ack: true, from: id, val: 0}); adapterStates.setState(id + '.cputime', {ack: true, from: id, val: 0}); adapterStates.setState(id + '.memRss', {val: 0, ack: true, from: id}); adapterStates.setState(id + '.memHeapTotal', {val: 0, ack: true, from: id}); adapterStates.setState(id + '.memHeapUsed', {val: 0, ack: true, from: id}); adapterStates.setState(id + '.eventLoopLag', {val: 0, ack: true, from: id}); this.outputCount += 6; } else { tools.measureEventLoopLag(1000, lag => this.eventLoopLags.push(lag)); } } } if (adapterConfig && adapterConfig.common && adapterConfig.common.restartSchedule) { try { schedule = require('node-schedule'); } catch { logger.error(this.namespaceLog + ' Cannot load node-schedule. Scheduled restart is disabled'); } if (schedule) { logger.debug(this.namespaceLog + ' Schedule restart: ' + adapterConfig.common.restartSchedule); restartScheduleJob = schedule.scheduleJob(adapterConfig.common.restartSchedule, () => { logger.info(this.namespaceLog + ' Scheduled restart.'); stop(false, true); }); } } // auto oStates if (options.states) { this.getStates('*', null, (err, _states) => { this.oStates = _states; this.subscribeStates('*'); if (firstConnection) { firstConnection = false; typeof options.ready === 'function' && options.ready(); this.emit('ready'); } else { typeof options.reconnect === 'function' && options.reconnect(); this.emit('reconnect'); } this.adapterReady = true; }); } else { typeof options.ready === 'function' && options.ready(); this.emit('ready'); this.adapterReady = true; // todo remove it later, when the error is fixed adapterStates.subscribe(this.namespace + '.checkLogging'); } }); }); }); }; const reportStatus = () => { if (!adapterStates) { return; } const id = 'system.adapter.' + this.namespace; adapterStates.setState(id + '.alive', {val: true, ack: true, expire: Math.floor(config.system.statisticsInterval / 1000) + 10, from: id}); this.outputCount++; if (this.connected) { adapterStates.setState(id + '.connected', {val: true, ack: true, expire: 30, from: id}); this.outputCount++; } if (!this.startedInCompactMode) { // pidUsage([pid,pid,...], function (err, stats) { // => { // cpu: 10.0, // percentage (from 0 to 100*vcore) // memory: 357306368, // bytes // ppid: 312, // PPID // pid: 727, // PID // ctime: 867000, // ms user + system time // elapsed: 6650000, // ms since the start of the process // timestamp: 864000000 // ms since epoch // } pidUsage(process.pid, (err, stats) => { // sometimes adapter is stopped, but this is still running if (!err && this && adapterStates && adapterStates.setState && stats) { adapterStates.setState(id + '.cpu', {ack: true, from: id, val: Math.round(100 * parseFloat(stats.cpu)) / 100}); adapterStates.setState(id + '.cputime', {ack: true, from: id, val: stats.ctime / 1000}); this.outputCount += 2; } }); try { //RSS is the resident set size, the portion of the process's memory held in RAM (as opposed to the swap space or the part held in the filesystem). const mem = process.memoryUsage(); adapterStates.setState(id + '.memRss', { val: parseFloat((mem.rss / 1048576/* 1MB */).toFixed(2)), ack: true, from: id }); adapterStates.setState(id + '.memHeapTotal', { val: parseFloat((mem.heapTotal / 1048576/* 1MB */).toFixed(2)), ack: true, from: id }); adapterStates.setState(id + '.memHeapUsed', { val: parseFloat((mem.heapUsed / 1048576/* 1MB */).toFixed(2)), ack: true, from: id }); } catch (err) { logger.warn(`${this.namespaceLog} Could not query used process memory: ${err.message}`); } this.outputCount += 3; if (this.eventLoopLags.length) { const eventLoopLag = Math.ceil(this.eventLoopLags.reduce((a, b) => (a + b)) / this.eventLoopLags.length); adapterStates.setState(id + '.eventLoopLag', {val: eventLoopLag, ack: true, from: id}); // average of measured values this.eventLoopLags = []; this.outputCount++; } } this.outputCount += 3; adapterStates.setState(id + '.uptime', {val: parseInt(process.uptime().toFixed(), 10), ack: true, from: id}); adapterStates.setState(id + '.inputCount', {val: this.inputCount, ack: true, from: id}); adapterStates.setState(id + '.outputCount', {val: this.outputCount, ack: true, from: id}); this.inputCount = 0; this.outputCount = 0; }; const stop = (isPause, isScheduled, exitCode, updateAliveState) => { exitCode = exitCode || (isScheduled ? EXIT_CODES.START_IMMEDIATELY_AFTER_STOP : 0); if (updateAliveState === undefined) { updateAliveState = true; } if (!stopInProgress || config.isInstall) { // when interval is deleted we already had a stop call before stopInProgress = true; reportInterval && clearInterval(reportInterval); reportInterval = null; const id = 'system.adapter.' + this.namespace; const finishUnload = () => { let tIDs = Object.keys(timers); if (tIDs.length) { logger.warn(`${this.namespaceLog} Found uncleared timeouts (report to developer): ${tIDs.join(', ')}`); tIDs.forEach(id => clearTimeout(timers[id])); timers = {}; } tIDs = Object.keys(intervals); if (tIDs.length) { logger.warn(`${this.namespaceLog} Found uncleared intervals (report to developer): ${tIDs.join(', ')}`); tIDs.forEach(id => clearInterval(intervals[id])); intervals = {}; } if (this.terminated) { return; } if (adapterStates && updateAliveState) { this.outputCount++; adapterStates.setState(id + '.alive', {val: false, ack: true, from: id}, () => { if (!isPause && logger) { logger.info(this.namespaceLog + ' terminating'); } this.terminate(exitCode); }); } else { if (!isPause && this.log) { logger.info(this.namespaceLog + ' terminating'); } this.terminate(exitCode); } }; if (typeof options.unload === 'function') { if (options.unload.length >= 1) { // The method takes (at least) a callback options.unload(finishUnload); } else { // The method takes no arguments, so it must return a Promise const unloadPromise = options.unload(); if (unloadPromise instanceof Promise) { // Call finishUnload in the case of success and failure unloadPromise.then(finishUnload, finishUnload); } else { // No callback accepted and no Promise returned - force unload logger.error(`${this.namespaceLog} Error in ${id}: The unload method must return a Promise if it does not accept a callback!`); } } } else { this.emit('unload', finishUnload); } // Even if the developer forgets to call the unload callback, we need to stop the process // Therefore wait a short while and then force the unload setTimeout(() => { if (adapterStates) { finishUnload(); // Give 1 seconds to write the value setTimeout(() => { if (!isPause && this.log) { logger.info(this.namespaceLog + ' terminating with timeout'); } this.terminate(exitCode); }, 1000); } else { if (!isPause && this.log) { logger.info(this.namespaceLog + ' terminating'); } this.terminate(exitCode); } }, (this.common && this.common.stopTimeout) || 500); } }; const exceptionHandler = async (err, isUnhandledRejection) => { // If the adapter has a callback to listen for unhandled errors // give it a chance to handle the error itself instead of restarting it if (typeof options.error === 'function') { try { // if error handler in the adapter returned exactly true, // we expect the error to be handled and do nothing more const wasHandled = options.error(err); if (wasHandled === true) { return; } } catch (e) { console.error(`Error in adapter error handler: ${e.message}`); } } // catch it on windows if (this.getPortRunning && err && err.message === 'listen EADDRINUSE') { logger.warn(this.namespaceLog + ' Port ' + this.getPortRunning.port + (this.getPortRunning.host ? ' for host ' + this.getPortRunning.host : '') + ' is in use. Get next'); setImmediate(() => this.getPort(this.getPortRunning.port + 1, this.getPortRunning.host, this.getPortRunning.callback)); return; } if (isUnhandledRejection) { logger.error(`${this.namespaceLog} Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().`); } logger.error(`${this.namespaceLog} ${isUnhandledRejection ? 'unhandled promise rejection' : 'uncaught exception'}: ${err ? err.message : err}`); if (err && err.stack) { logger.error(`${this.namespaceLog} ${err.stack}`); } if (err) { const message = err.code ? `Exception-Code: ${err.code}: ${err.message}` : err.message; logger.error(`${this.namespaceLog} ${message}`); try { await this.registerNotification('system', null, message); } catch { // ignore } } try { stop(false, false, EXIT_CODES.UNCAUGHT_EXCEPTION, false); setTimeout(() => this.terminate(EXIT_CODES.UNCAUGHT_EXCEPTION), 1000); } catch (err) { logger.error(`${this.namespaceLog} exception by stop: ${err ? err.message : err}`); } }; process.once('SIGINT', stop); process.once('SIGTERM', stop); // And the exit event shuts down the child. process.once('exit', stop); process.on('uncaughtException', err => exceptionHandler(err)); process.on('unhandledRejection', err => exceptionHandler(err, true)); const pluginSettings = { scope: 'adapter', namespace: `system.adapter.${this.namespace}`, logNamespace: this.namespaceLog, log: logger, iobrokerConfig: config, parentPackage: this.pack, controllerVersion }; this.pluginHandler = new PluginHandler(pluginSettings); this.pluginHandler.addPlugins(this.ioPack.common.plugins, [this.adapterDir, __dirname]); // first resolve from adapter directory, else from js-controller initObjects(() => { if (this.inited) { this.log && logger.warn(this.namespaceLog + ' Reconnection to DB.'); return; } this.inited = true; // auto oObjects if (options.objects) { this.getAdapterObjects(objs => { this.oObjects = objs; this.subscribeObjects('*'); initStates(prepareInitAdapter); }); } else { initStates(prepareInitAdapter); } }); return this; } // extend the EventEmitter class using our class util.inherits(Adapter, EventEmitter); module.exports = Adapter;