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