yunkong2.js-controller/lib/adapter.js
2018-09-17 20:32:19 +08:00

5103 lines
220 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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
var net = require('net');
var fs = require('fs');
var extend = require('node.extend');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var tools = require(__dirname + '/tools');
var getConfigFileName = tools.getConfigFileName;
var schedule;
var password = require(__dirname + '/password');
var config = null;
var that;
var defaultObjs;
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;
var regUser = /^system\.user\./;
var regGroup = /^system\.group\./;
that = this;
that.logList = [];
// possible arguments
// 0,1,.. - instance
// info, debug, warn, error - log level
// --force
// --logs
// --silent
// --install
if (process.argv) {
for (var 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;
}
var 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();
var 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
var 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)) {
var 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;
var 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');
}
var 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');
}
var os = require('os');
var ifaces = os.networkInterfaces();
var ipArr = [];
for (var dev in ifaces) {
if (!ifaces.hasOwnProperty(dev)) continue;
/*jshint loopfunc:true */
ifaces[dev].forEach(function (details) {
if (!details.internal) ipArr.push(details.address);
});
}
var 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;
var reportInterval;
var 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';
port = parseInt(port, 10);
that.getPortRunning = {port: port, callback: callback};
var server = net.createServer();
try {
server.listen(port, function (/* err */) {
server.once('close', function () {
if (typeof callback === 'function') {
//that.getPortRunning = null;
callback(port);
}
});
server.close();
});
server.on('error', function (/* err */) {
setTimeout(function () {
that.getPort(port + 1, callback);
}, 100);
});
} catch (e) {
setImmediate(function () {
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, function (err, obj) {
if (err || !obj || !obj.common || (!obj.common.enabled && user !== 'system.user.admin')) {
callback(false);
return;
}
password(pw).check(obj.common.password, function (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, function (err, obj) {
if (err || !obj) {
if (typeof callback === 'function') callback('User does not exist');
return;
}
password(pw).hash(null, null, function (err, res) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
that.extendForeignObject(user, {
common: {
password: res
}
}, options, function () {
if (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;
}
that.getForeignObject(user, options, function (err, obj) {
if (err || !obj) {
callback(false);
return;
}
that.getForeignObject(group, options, function (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 {object} 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
var acl = {user: user};
if (user === 'system.user.admin') {
acl.groups = ['system.group.administrator'];
for (var 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, function (err, groups) {
// aggregate all groups permissions, where this user is
if (groups) {
for (var 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;
}
var gAcl = groups[g].common.acl;
try {
for (var 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 (var 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, function (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', function (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('Not found');
} else {
var ca;
if (chainedName) {
var chained = readFileCertificate(obj.native.certificates[chainedName]).split('-----END CERTIFICATE-----\r\n');
ca = [];
for (var 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);
// Can be later deleted if no more appears
that.inited = false;
initObjects(function () {
if (that.inited) {
if (that.log) that.log.warn('Reconnection to DB.');
return;
}
that.inited = true;
// auto oObjects
if (options.objects) {
that.getAdapterObjects(function (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 {
var obj = objs.shift();
that.getObject(obj._id, function (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, function (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', function (err, res) {
if (options.instance !== undefined) {
initAdapter(options);
} else
if (!config.isInstall && res && res.val === true) {
logger.error(options.name + '.' + instance + ' already running');
process.exit(7);
} else {
that.getForeignObject('system.adapter.' + that.namespace, function (err, res) {
if ((err || !res) && !config.isInstall) {
logger.error(options.name + '.' + instance + ' invalid config');
process.exit(2);
} else {
createInstancesObjects(function () {
initAdapter(res);
});
}
});
}
});
}
function autoSubscribeOn(cb) {
if (!that.autoSubscribe) {
// collect all
that.objects.getObjectView('system', 'instance', {startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, options, function (err, res) {
if (res && res.rows) {
that.autoSubscribe = [];
for (var c = res.rows.length - 1; c >= 0; c--) {
if (res.rows[c].value.common.subscribable) {
var _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: function () {
that.connected = true;
// Read dateformat if using of formatDate is announced
if (options.useFormatDate) {
that.getForeignObject('system.config', function (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: function () {
that.connected = false;
},
change: function (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(function () {
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) {
var _id = id.substring(15); // 'system.adapter.'.length
if (obj.common.enabled) {
if (that.autoSubscribe.indexOf(_id) === -1) that.autoSubscribe.push(_id);
} else {
var 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(function () {
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(function () {
if (typeof options.objectChange === 'function') options.objectChange(id, obj);
// emit 'objectChange' event instantly
that.emit('objectChange', id, obj);
});
}
},
connectTimeout: function (/* 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 */) {
var 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) {
logger.error(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.ts) obj.ts = new Date().getTime();
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) {
var objects = {};
that.objects.getObjectView('system', 'state', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, function (err, _states) {
that.objects.getObjectView('system', 'channel', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, function (err, _channels) {
that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, function (err, _devices) {
if (_channels) {
for (var c = _channels.rows.length - 1; c >= 0; c--) {
objects[_channels.rows[c].id] = _channels.rows[c].value;
}
}
if (_devices) {
for (var 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 (var 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.getAdapterObjectsAsync = tools.promisifyNoError(that.getAdapterObjects, that);
/**
* 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 {function} callback return result
* <pre><code>
* function (err, obj) {
* if (err) adapter.log.error(err);
* // obj is {"id": id}
* }
* </code></pre>
*/
that.extendObject = function extendObject(id, obj, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
id = that._fixId(id, false, obj.type);
if (obj.children || obj.parent) {
logger.warn(that.namespace + ' Do not use parent or children for ' + id);
}
// 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))
) {
// Read whole object
that.objects.getObject(id, options, function (err, oldObj) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
if (!oldObj) {
logger.error(that.namespace + ' 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 = extend(true, oldObj, obj);
if (!obj.from) obj.from = 'system.adapter.' + that.namespace;
if (!obj.ts) obj.ts = new Date().getTime();
that.objects.setObject(id, obj, options, callback);
});
} else {
if (!obj.from) obj.from = 'system.adapter.' + that.namespace;
if (!obj.ts) obj.ts = new Date().getTime();
that.objects.extendObject(id, obj, options, callback);
}
};
/**
* Promise-version of Adapter.extendObject
*/
that.extendObjectAsync = tools.promisify(that.extendObject, that);
/**
* 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 {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.setForeignObject = function setForeignObject(id, obj, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!obj.from) obj.from = 'system.adapter.' + that.namespace;
if (!obj.ts) obj.ts = new Date().getTime();
that.objects.setObject(id, obj, options, callback);
};
/**
* Promise-version of Adapter.setForeignObject
*/
that.setForeignObjectAsync = tools.promisify(that.setForeignObject, that);
/**
* 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
* @param {function} callback return result
* <pre><code>
* function (err, obj) {
* // obj is {"id": id}
* if (err) adapter.log.error(err);
* }
* </code></pre>
*/
that.extendForeignObject = function extendForeignObject(id, obj, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
// delete arrays if they should be changed
if (obj && ((obj.native && (obj.native.repositories || obj.native.certificates || obj.native.devices)) ||
(obj.common && obj.common.members))) {
// Read whole object
that.objects.getObject(id, options, function (err, oldObj) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
if (!oldObj) {
logger.error(that.namespace + ' 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 = extend(true, oldObj, obj);
if (!obj.from) obj.from = 'system.adapter.' + that.namespace;
if (!obj.ts) obj.ts = new Date().getTime();
that.objects.setObject(id, obj, callback);
});
} else {
if (!obj.from) obj.from = 'system.adapter.' + that.namespace;
if (!obj.ts) obj.ts = new Date().getTime();
that.objects.extendObject(id, obj, options, callback);
}
};
/**
* Promise-version of Adapter.extendForeignObject
*/
that.extendForeignObjectAsync = tools.promisify(that.extendForeignObject, that);
/**
* 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 {function} callback return result
* <pre><code>
* function (err, obj) {
* if (err) adapter.log.error('Cannot get object: ' + err);
* }
* </code></pre>
*/
that.getObject = function getObject(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.getObject(that._fixId(id), options, callback);
};
/**
* Promise-version of Adapter.getObject
*/
that.getObjectAsync = tools.promisify(that.getObject, that);
/**
* 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 {function} 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>
*/
that.getEnum = function getEnum(_enum, options, callback) {
if (typeof _enum === 'function') {
callback = _enum;
options = null;
_enum = '';
}
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!_enum.match('^enum.')) _enum = 'enum.' + _enum;
var result = {};
that.objects.getObjectView('system', 'enum', {startkey: _enum + '.', endkey: _enum + '.\u9999'}, options, function (err, res) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
for (var t = 0; t < res.rows.length; t++) {
result[res.rows[t].id] = res.rows[t].value;
}
if (typeof callback === 'function') callback(err, result, _enum);
});
};
/**
* Promise-version of Adapter.getEnum
*/
that.getEnumAsync = tools.promisify(that.getEnum, that, ["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 {function} 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>
*/
that.getEnums = function getEnums(_enumList, options, callback) {
if (typeof _enumList === 'function') {
callback = _enumList;
_enumList = null;
}
if (typeof options === 'function') {
callback = options;
options = null;
}
var _enums = {};
if (_enumList) {
if (typeof _enumList === 'string') _enumList = [_enumList];
var count = 0;
for (var t = 0; t < _enumList.length; t++) {
count++;
that.getEnum(_enumList[t], options, function (err, list, _enum) {
if (list) _enums[_enum] = list;
if (!--count && callback) callback(err, _enums);
});
}
} else {
// Read all enums
that.objects.getObjectView('system', 'enum', {startkey: 'enum.', endkey: 'enum.\u9999'}, options, function (err, res) {
if (err) {
callback(err);
return;
}
var result = {};
if (res && res.rows) {
for (var i = 0; i < res.rows.length; i++) {
var 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;
}
}
if (callback) callback(err, result);
});
}
};
/**
* Promise-version of Adapter.getEnums
*/
that.getEnumsAsync = tools.promisify(that.getEnums, that);
/**
* 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 {function} callback return result
* <pre><code>
* function (err, obj) {
* if (err) adapter.log.error('Cannot get object: ' + err);
* }
* </code></pre>
*/
that.getForeignObjects = function getForeignObjects(pattern, type, enums, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
var params = {};
if (pattern && pattern !== '*') {
params = {
startkey: pattern.replace('*', ''),
endkey: pattern.replace('*', '\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' && !(enums instanceof Array)) {
options = enums;
enums = null;
}
that.objects.getObjectView('system', type || 'state', params, options, function (err, res) {
if (err) {
callback(err);
return;
}
that.getEnums(enums, function (err, _enums) {
var list = {};
for (var i = 0; i < res.rows.length; i++) {
var id = res.rows[i].id;
if (typeof id !== 'string') {
that.log.debug('Invalid id returned from getEnums: ' + JSON.stringify(id));
continue;
}
list[id] = res.rows[i].value;
if (_enums && id) {
// get device or channel of this state and check it too
var parts = id.split('.');
parts.splice(parts.length - 1, 1);
var channel = parts.join('.');
parts.splice(parts.length - 1, 1);
var device = parts.join('.');
list[id].enums = {};
for (var es in _enums) {
if (!_enums.hasOwnProperty(es)) continue;
for (var e in _enums[es]) {
if (!_enums[es].hasOwnProperty(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;
}
}
}
}
}
callback(null, list);
});
});
};
/**
* Promise-version of Adapter.getForeignObjects
*/
that.getForeignObjectsAsync = tools.promisify(that.getForeignObjects, that);
/**
* 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 {function} 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>
*/
that.findForeignObject = function findForeignObject(id, type, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.findObject(id, type, options, callback);
};
/**
* Promise-version of Adapter.findForeignObject
*/
that.findForeignObjectAsync = tools.promisify(that.findForeignObject, that, ["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 {function} callback return result
* <pre><code>
* function (err, obj) {
* if (err) adapter.log.error('Cannot get object: ' + err);
* }
* </code></pre>
*/
that.getForeignObject = function getForeignObject(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.getObject(id, options, callback);
};
/**
* Promise-version of Adapter.getForeignObject
*/
that.getForeignObjectAsync = tools.promisify(that.getForeignObject, that);
/**
* Delete 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 delObject
* @memberof Adapter
* @param {string} id exactly object ID (without namespace)
* @param {object} options optional user context
* @param {function} callback return result
* <pre><code>
* function (err) {
* if (err) adapter.log.error('Cannot delete object: ' + err);
* }
* </code></pre>
*/
that.delObject = function delObject(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.delObject(that._fixId(id), options, callback);
};
/**
* Promise-version of Adapter.delObject
*/
that.delObjectAsync = tools.promisify(that.delObject, that);
/**
* Delete any object.
*
* ID must be specified with namespace.
*
* @alias delForeignObject
* @memberof Adapter
* @param {string} id exactly object ID (with namespace)
* @param {object} options optional user context
* @param {function} callback return result
* <pre><code>
* function (err) {
* if (err) adapter.log.error('Cannot delete object: ' + err);
* }
* </code></pre>
*/
that.delForeignObject = function delForeignObject(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.delObject(id, options, callback);
};
/**
* Promise-version of Adapter.delForeignObject
*/
that.delForeignObjectAsync = tools.promisify(that.delForeignObject, that);
/**
* 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
*/
that.subscribeObjects = function subscribeObjects(pattern, options) {
if (pattern === '*') {
that.objects.subscribe(that.namespace + '.*');
} else {
pattern = that._fixId(pattern, true);
that.objects.subscribe(pattern, options);
}
};
/**
* 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
*/
that.unsubscribeObjects = function unsubscribeObjects(pattern, options) {
if (pattern === '*') {
that.objects.unsubscribe(that.namespace + '.*', options);
} else {
pattern = that._fixId(pattern, true);
that.objects.unsubscribe(pattern);
}
};
/**
* Subscribe for the changes of objects in any instance.
*
* @alias subscribeForeignObjects
* @memberof Adapter
* @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces
* @param {object} options optional user context
*/
that.subscribeForeignObjects = function subscribeObjects(pattern, options) {
that.objects.subscribe(pattern, options);
};
/**
* 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
*/
that.unsubscribeForeignObjects = function unsubscribeForeignObjects(pattern, options) {
if (!pattern) pattern = '*';
that.objects.unsubscribe(pattern, options);
};
that.setObjectNotExists = function setObjectNotExists(id, object, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
id = that._fixId(id);
if (object.children || object.parent) {
logger.warn(that.namespace + ' Do not use parent or children for ' + id);
}
that.objects.getObject(id, options, function (err, obj) {
if (!obj) {
if (!object.from) object.from = 'system.adapter.' + that.namespace;
if (!object.ts) object.ts = new Date().getTime();
that.objects.setObject(id, object, callback);
} else {
if (typeof callback === 'function') callback(null);
}
});
};
/**
* Promise-version of Adapter.setObjectNotExists
*/
that.setObjectNotExistsAsync = tools.promisify(that.setObjectNotExists, that);
that.setForeignObjectNotExists = function setForeignObjectNotExists(id, obj, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.getObject(id, options, function (err, _obj) {
if (!_obj) {
if (!obj.from) obj.from = 'system.adapter.' + that.namespace;
if (!obj.ts) obj.ts = new Date().getTime();
that.objects.setObject(id, obj, callback);
} else {
if (typeof callback === 'function') callback(null);
}
});
};
/**
* Promise-version of Adapter.setForeignObjectNotExists
*/
that.setForeignObjectNotExistsAsync = tools.promisify(that.setForeignObjectNotExists, that);
that._DCS2ID = function (device, channel, stateOrPoint) {
var id = '';
if (device) id += device;
if (channel) id += ((id) ? '.' : '') + channel;
if (stateOrPoint !== true && stateOrPoint !== false) {
if (stateOrPoint) id += ((id) ? '.' : '') + stateOrPoint;
} else if (stateOrPoint === true) {
if (id) id += '.';
}
return id;
};
that.createDevice = function createDevice(deviceName, common, _native, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!deviceName) {
that.log.error('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(/[.\s]+/g, '_');
_native = _native || {};
that.setObjectNotExists(deviceName, {
type: 'device',
common: common,
native: _native
}, options, callback);
};
/**
* Promise-version of Adapter.createDevice
*/
that.createDeviceAsync = tools.promisify(that.createDevice, that);
// name of channel must be in format "channel"
that.createChannel = function createChannel(parentDevice, channelName, roleOrCommon, _native, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!channelName) throw 'Try to create channel without name!';
if (typeof _native === 'function') {
callback = _native;
_native = {};
}
if (typeof roleOrCommon === 'function') {
callback = roleOrCommon;
roleOrCommon = undefined;
}
var common = {};
if (typeof roleOrCommon === 'string') {
common = {
role: roleOrCommon
};
} else if (typeof roleOrCommon === 'object') {
common = roleOrCommon;
}
common.name = common.name || channelName;
if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_');
channelName = channelName.replace(/[.\s]+/g, '_');
channelName = that._DCS2ID(parentDevice, channelName);
_native = _native || {};
var obj = {
type: 'channel',
common: common,
native: _native
};
that.setObjectNotExists(channelName, obj, options, callback);
};
/**
* Promise-version of Adapter.createChannel
*/
that.createChannelAsync = tools.promisify(that.createChannel, that);
that.createState = function createState(parentDevice, parentChannel, stateName, roleOrCommon, _native, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!stateName) throw 'Empty name is not allowed!';
if (typeof _native === 'function') {
callback = _native;
_native = {};
}
if (typeof roleOrCommon === 'function') {
callback = roleOrCommon;
roleOrCommon = undefined;
}
var common = {};
if (typeof roleOrCommon === 'string') {
common = {
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(that.namespace + ' Try to create state ' + (parentDevice ? (parentDevice + '.') : '') + parentChannel + '.' + stateName + ' without role');
return;
}
if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_');
if (parentChannel) parentChannel = parentChannel.replace(/[.\s]+/g, '_');
stateName = stateName.replace(/[.\s]+/g, '_');
var id = that._fixId({device: parentDevice, channel: parentChannel, state: stateName});
// Check min, max and def values for number
if (common.type !== undefined && common.type === 'number') {
var min = 0;
var max = 0;
var def = 0;
var 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(that.namespace + ' ' + err);
if (callback) callback(err);
return;
} 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(that.namespace + ' ' + err);
if (callback) callback(err);
return;
} else {
common.max = max;
}
}
}
if (common.def !== undefined) {
def = common.def;
if (typeof def !== 'number') {
def = parseFloat(def);
if (isNaN(def)) {
err = 'Wrong type of ' + id + '.common.def';
logger.error(that.namespace + ' ' + err);
if (callback) callback(err);
return;
} 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;
}
that.setObjectNotExists(id, {
type: 'state',
common: common,
native: _native
}, options, callback);
if (common.def !== undefined) {
that.setState(id, common.def, options);
} else {
that.setState(id, null, true, options);
}
};
/**
* Promise-version of Adapter.createState
*/
that.createStateAsync = tools.promisify(that.createState, that);
that.deleteDevice = function deleteDevice(deviceName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
deviceName = deviceName.replace(/[.\s]+/g, '_');
if (!that._namespaceRegExp.test(deviceName)) deviceName = that.namespace + '.' + deviceName;
that.objects.getObjectView('system', 'device', {startkey: deviceName, endkey: deviceName}, options, function (err, res) {
if (err || !res || !res.rows) {
if (typeof callback === 'function') callback(err);
callback = null;
return;
}
var cnt = 0;
if (res.rows.length > 1) that.log.warn('Found more than one device ' + deviceName);
for (var t = 0; t < res.rows.length; t++) {
cnt++;
that.delObject(res.rows[t].id, options, function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
callback = null;
return;
}
if (!--cnt) {
that.objects.getObjectView('system', 'channel', {startkey: deviceName + '.', endkey: deviceName + '.\u9999'}, options, function (err, res) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
var _cnt = 0;
for (var k = 0; k < res.rows.length; k++) {
_cnt++;
that.deleteChannel(deviceName, res.rows[k].id, options, function (err) {
if (!(--_cnt) && callback) {
callback(err);
} else {
if (err) {
if (typeof callback === 'function') callback(err);
callback = null;
}
}
});
}
if (!_cnt && callback) callback();
});
}
});
}
if (!cnt && callback) callback();
});
};
/**
* Promise-version of Adapter.deleteDevice
*/
that.deleteDeviceAsync = tools.promisify(that.deleteDevice, that);
that.addChannelToEnum = function addChannelToEnum(enumName, addTo, parentDevice, channelName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (parentDevice) {
if (that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (that._namespaceRegExp.test(channelName)) {
channelName = channelName.substring(that.namespace.length + 1);
}
if (parentDevice && channelName.substring(0, parentDevice.length) === parentDevice) {
channelName = channelName.substring(parentDevice.length + 1);
}
channelName = channelName.replace(/[.\s]+/g, '_');
var objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName);
if (addTo.match(/^enum\./)) {
that.objects.getObject(addTo, options, function (err, obj) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
if (!err && obj) {
var pos = obj.common.members.indexOf(objId);
if (pos === -1) {
obj.common.members.push(objId);
obj.from = 'system.adapter.' + that.namespace;
obj.ts = new Date().getTime();
that.objects.setObject(obj._id, obj, options, function (err) {
if (callback) callback(err);
});
}
}
});
} else {
if (enumName.match(/^enum\./)) enumName = enumName.substring(5);
that.objects.getObject('enum.' + enumName + '.' + addTo, options, function (err, obj) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
if (obj) {
var pos = obj.common.members.indexOf(objId);
if (pos === -1) {
obj.common.members.push(objId);
obj.from = 'system.adapter.' + that.namespace;
obj.ts = new Date().getTime();
that.objects.setObject(obj._id, obj, options, callback);
} else {
if (callback) callback();
}
} else {
// Create enum
that.objects.setObject('enum.' + enumName + '.' + addTo, {
common: {
name: addTo,
members: [objId]
},
from: 'system.adapter.' + that.namespace,
ts: new Date().getTime(),
type: 'enum'
}, options, callback);
}
});
}
};
/**
* Promise-version of Adapter.addChannelToEnum
*/
that.addChannelToEnumAsync = tools.promisify(that.addChannelToEnum, that);
that.deleteChannelFromEnum = function deleteChannelFromEnum(enumName, parentDevice, channelName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (parentDevice) {
if (parentDevice.substring(0, that.namespace.length) === that.namespace) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (channelName && channelName.substring(0, that.namespace.length) === that.namespace) {
channelName = channelName.substring(that.namespace.length + 1);
}
if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) {
channelName = channelName.substring(parentDevice.length + 1);
}
channelName = channelName || '';
channelName = channelName.replace(/[.\s]+/g, '_');
var objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName);
if (enumName) {
enumName = 'enum.' + enumName + '.';
} else {
enumName = 'enum.';
}
that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, function (err, res) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
if (res) {
var count = 0;
for (var i = 0; i < res.rows.length; i++) {
count++;
that.objects.getObject(res.rows[i].id, options, function (err, obj) {
if (err) {
if (typeof callback === 'function') callback(err);
callback = null;
return;
}
if (!err && obj && obj.common && obj.common.members) {
var pos = obj.common.members.indexOf(objId);
if (pos !== -1) {
obj.common.members.splice(pos, 1);
count++;
obj.from = 'system.adapter.' + that.namespace;
obj.ts = new Date().getTime();
that.objects.setObject(obj._id, obj, options, function (err) {
if (!--count && callback) {
callback(err);
} else {
if (err) {
if (typeof callback === 'function') callback(err);
callback = null;
}
}
});
}
}
if (!--count && callback) callback(err);
});
}
} else if (callback) {
callback (err);
}
});
};
/**
* Promise-version of Adapter.deleteChannelFromEnum
*/
that.deleteChannelFromEnumAsync = tools.promisify(that.deleteChannelFromEnum, that);
that.deleteChannel = function 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 (!parentDevice) parentDevice = '';
that.deleteChannelFromEnum('', parentDevice, channelName);
var _parentDevice = parentDevice;
var _channelName = channelName;
if (parentDevice) {
if (that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (channelName && that._namespaceRegExp.test(channelName)) {
channelName = channelName.substring(that.namespace.length + 1);
}
if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) {
channelName = channelName.substring(parentDevice.length + 1);
}
channelName = channelName || '';
channelName = channelName.replace(/[.\s]+/g, '_');
channelName = that.namespace + '.' + that._DCS2ID(parentDevice, channelName);
logger.info(that.namespace + ' Delete channel ' + channelName);
that.objects.getObjectView('system', 'channel', {startkey: channelName, endkey: channelName}, options, function (err, res) {
if (err || !res || !res.rows) {
if (typeof callback === 'function') callback(err);
callback = null;
return;
}
var cnt = 0;
if (res.rows.length > 1) that.log.warn('Found more than one channel ' + channelName);
for (var t = 0; t < res.rows.length; t++) {
cnt++;
that.delObject(res.rows[t].id, options, function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
callback = null;
return;
}
if (!--cnt) {
that.objects.getObjectView('system', 'state', {startkey: channelName + '.', endkey: channelName + '.\u9999'}, options, function (err, res) {
if (err || !res || !res.rows) {
if (typeof callback === 'function') callback(err);
callback = null;
return;
}
var _cnt = 0;
for (var k = 0; k < res.rows.length; k++) {
_cnt++;
that.deleteState(_parentDevice, _channelName, res.rows[k].id, options, function (err) {
if (!--_cnt && callback) {
callback(err);
} else {
if (err) {
if (typeof callback === 'function') callback(err);
callback = null;
}
}
});
}
if (!_cnt && callback) callback();
});
}
});
}
if (!cnt && callback) callback();
});
};
/**
* Promise-version of Adapter.deleteChannel
*/
that.deleteChannelAsync = tools.promisify(that.deleteChannel, that);
that.deleteState = function 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 = '';
}
}
that.deleteStateFromEnum('', parentDevice, parentChannel, stateName, options);
if (parentDevice) {
if (that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (parentChannel) {
if (that._namespaceRegExp.test(parentChannel)) {
parentChannel = parentChannel.substring(that.namespace.length + 1);
}
if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) {
parentChannel = parentChannel.substring(parentDevice.length + 1);
}
parentChannel = parentChannel.replace(/[.\s]+/g, '_');
}
if (that._namespaceRegExp.test(stateName)) {
stateName = stateName.substring(that.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(/[.\s]+/g, '_');
var _name = that._DCS2ID(parentDevice, parentChannel, stateName);
that.delState(_name, options, function () {
that.delObject(_name, options, callback);
});
};
/**
* Promise-version of Adapter.deleteState
*/
that.deleteStateAsync = tools.promisify(that.deleteState, that);
that.getDevices = function getDevices(options, callback) {
if (typeof options === 'function' && typeof callback === 'object') {
var tmp = callback;
callback = options;
options = tmp;
}
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999'}, options, function (err, obj) {
if (callback) {
if (obj.rows.length) {
var res = [];
for (var i = 0; i < obj.rows.length; i++) {
res.push(obj.rows[i].value);
}
callback(null, res);
} else {
callback(err, []);
}
}
});
};
/**
* Promise-version of Adapter.getDevices
*/
that.getDevicesAsync = tools.promisify(that.getDevices, that);
that.getChannelsOf = function getChannelsOf(parentDevice, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (typeof parentDevice === 'function') {
callback = parentDevice;
parentDevice = null;
}
if (!parentDevice) parentDevice = '';
if (parentDevice && that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
parentDevice = that.namespace + (parentDevice ? ('.' + parentDevice) : '');
that.objects.getObjectView('system', 'channel', {startkey: parentDevice + '.', endkey: parentDevice + '.\u9999'}, options, function (err, obj) {
if (callback) {
if (obj.rows.length) {
var res = [];
for (var i = 0; i < obj.rows.length; i++) {
res.push(obj.rows[i].value);
}
callback(null, res);
} else {
callback(err, []);
}
}
});
};
/**
* Promise-version of Adapter.getChannelsOf
*/
that.getChannelsOfAsync = tools.promisify(that.getChannelsOf, that);
that.getChannels = that.getChannelsOf;
that.getStatesOf = function 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 (!parentDevice) {
parentDevice = '';
} else {
if (that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (!parentChannel) {
parentChannel = '';
} else if (that._namespaceRegExp.test(parentChannel)) {
parentChannel = parentChannel.substring(that.namespace.length + 1);
}
if (parentDevice && parentChannel && parentChannel.substring(0, parentDevice.length) === parentDevice) {
parentChannel = parentChannel.substring(parentDevice.length + 1);
}
parentChannel = parentChannel.replace(/[.\s]+/g, '_');
var id = that.namespace + '.' + that._DCS2ID(parentDevice, parentChannel, true);
that.objects.getObjectView('system', 'state', {startkey: id, endkey: id + '\u9999'}, options, function (err, obj) {
if (callback) {
var res = [];
if (obj.rows.length) {
var read = 0;
for (var i = 0; i < obj.rows.length; i++) {
read++;
that.objects.getObject(obj.rows[i].id, function (err, subObj) {
if (subObj) res.push(subObj);
if (!--read) callback(null, res);
});
}
} else {
callback(null, res);
}
}
});
};
/**
* Promise-version of Adapter.getStatesOf
*/
that.getStatesOfAsync = tools.promisify(that.getStatesOf, that);
that.addStateToEnum = function addStateToEnum(enumName, addTo, parentDevice, parentChannel, stateName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (parentDevice) {
if (that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (parentChannel) {
if (that._namespaceRegExp.test(parentChannel)) {
parentChannel = parentChannel.substring(that.namespace.length + 1);
}
if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) {
parentChannel = parentChannel.substring(parentDevice.length + 1);
}
parentChannel = parentChannel.replace(/[.\s]+/g, '_');
}
if (that._namespaceRegExp.test(stateName)) {
stateName = stateName.substring(that.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(/[.\s]+/g, '_');
var objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName});
if (addTo.match(/^enum\./)) {
that.objects.getObject(addTo, options, function (err, obj) {
if (!err && obj) {
var pos = obj.common.members.indexOf(objId);
if (pos === -1) {
obj.common.members.push(objId);
obj.from = 'system.adapter.' + that.namespace;
obj.ts = new Date().getTime();
that.objects.setObject(obj._id, obj, options, callback);
} else if (callback) {
callback();
}
} else {
if (callback) callback(err || 'object not found');
}
});
} else {
if (enumName.match(/^enum\./)) enumName = enumName.substring(5);
that.objects.getObject('enum.' + enumName + '.' + addTo, options, function (err, obj) {
if (!err && obj) {
var pos = obj.common.members.indexOf(objId);
if (pos === -1) {
obj.common.members.push(objId);
obj.from = 'system.adapter.' + that.namespace;
obj.ts = new Date().getTime();
that.objects.setObject(obj._id, obj, callback);
} else if (callback) {
callback();
}
} else {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
// Create enum
that.objects.setObject('enum.' + enumName + '.' + addTo, {
common: {
name: addTo,
members: [objId]
},
from: 'system.adapter.' + that.namespace,
ts: new Date().getTime(),
type: 'enum'
}, options, callback);
}
});
}
};
/**
* Promise-version of Adapter.addStateToEnum
*/
that.addStateToEnumAsync = tools.promisify(that.addStateToEnum, that);
that.deleteStateFromEnum = function deleteStateFromEnum(enumName, parentDevice, parentChannel, stateName, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (parentDevice) {
if (that._namespaceRegExp.test(parentDevice)) {
parentDevice = parentDevice.substring(that.namespace.length + 1);
}
parentDevice = parentDevice.replace(/[.\s]+/g, '_');
}
if (parentChannel) {
if (that._namespaceRegExp.test(parentChannel)) {
parentChannel = parentChannel.substring(that.namespace.length + 1);
}
if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) {
parentChannel = parentChannel.substring(parentDevice.length + 1);
}
parentChannel = parentChannel.replace(/[.\s]+/g, '_');
}
if (that._namespaceRegExp.test(stateName)) {
stateName = stateName.substring(that.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(/[.\s]+/g, '_');
var objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}, false, 'state');
if (enumName) {
enumName = 'enum.' + enumName + '.';
} else {
enumName = 'enum.';
}
that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, function (err, res) {
if (!err && res) {
var count = 0;
for (var i = 0; i < res.rows.length; i++) {
count++;
that.objects.getObject(res.rows[i].id, options, function (err, obj) {
if (err) {
if (callback) {
callback(err);
callback = null;
}
return;
}
if (!err && obj && obj.common && obj.common.members) {
var pos = obj.common.members.indexOf(objId);
if (pos !== -1) {
obj.common.members.splice(pos, 1);
count++;
obj.from = 'system.adapter.' + that.namespace;
obj.ts = new Date().getTime();
that.objects.setObject(obj._id, obj, function (err) {
if (!--count && callback) {
callback(err);
callback = null;
}
});
}
}
if (!--count && callback) {
callback(err);
callback = null;
}
});
}
if (!count && callback) {
callback();
callback = null;
}
} else if (callback) {
callback(err);
callback = null;
}
});
};
/**
* Promise-version of Adapter.deleteStateFromEnum
*/
that.deleteStateFromEnumAsync = tools.promisify(that.deleteStateFromEnum, that);
that.chmodFile = function chmodFile(_adapter, path, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.chmodFile(_adapter, path, options, callback);
};
/**
* Promise-version of Adapter.chmodFile
*/
that.chmodFileAsync = tools.promisify(that.chmodFile, that, ["entries", "id"]);
/**
* 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 {function} 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>
*/
that.readDir = function readDir(_adapter, path, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.readDir(_adapter, path, options, callback);
};
/**
* Promise-version of Adapter.readDir
*/
that.readDirAsync = tools.promisify(that.readDir, that);
that.unlink = function unlink(_adapter, name, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.unlink(_adapter, name, options, callback);
};
/**
* Promise-version of Adapter.unlink
*/
that.unlinkAsync = tools.promisify(that.unlink, that);
that.delFile = that.unlink;
that.delFileAsync = that.unlinkAsync;
that.rename = function rename(_adapter, oldName, newName, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.rename(_adapter, oldName, newName, options, callback);
};
/**
* Promise-version of Adapter.rename
*/
that.renameAsync = tools.promisify(that.rename, that);
that.mkdir = function mkdir(_adapter, dirname, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.mkdir(_adapter, dirname, options, callback);
};
/**
* Promise-version of Adapter.mkdir
*/
that.mkdirAsync = tools.promisify(that.mkdir, that);
/**
* 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 {function} callback return result
* <pre><code>
* function (err, data) {
* // data is utf8 or binary Buffer depends on the file extension.
* }
* </code></pre>
*/
that.readFile = function readFile(_adapter, filename, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.readFile(_adapter, filename, options, callback);
};
/**
* Promise-version of Adapter.readFile
*/
that.readFileAsync = tools.promisify(that.readFile, that, ["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', 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} data data as UTF8 string or buffer depends on the file extension.
* @param {object} options optional user context
* @param {function} callback return result
* <pre><code>
* function (err) {
*
* }
* </code></pre>
*/
that.writeFile = function writeFile(_adapter, filename, data, options, callback) {
if (_adapter === null) _adapter = that.name;
if (typeof options === 'function') {
callback = options;
options = null;
}
that.objects.writeFile(_adapter, filename, data, options, callback);
};
/**
* Promise-version of Adapter.writeFile
*/
that.writeFileAsync = tools.promisify(that.writeFile, that);
that.formatValue = function (value, decimals, _format) {
if (typeof decimals !== 'number') {
_format = decimals;
decimals = 2;
}
var format = (!_format || _format.length !== 2) ? ((that.isFloatComma === undefined) ? '.,' : ((that.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]);
};
that.formatDate = function 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 '';
var type = typeof dateObj;
if (type === 'string') dateObj = new Date(dateObj);
if (type !== 'object') {
var 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);
}
}
var format = _format || that.dateFormat || 'DD.MM.YYYY';
if (isDuration) dateObj.setMilliseconds(dateObj.getMilliseconds() + dateObj.getTimezoneOffset() * 60 * 1000);
var validFormatChars = 'YJГMМDTДhSчmмsс';
var s = '';
var result = '';
function put(s) {
/** @type {number | string} */
var 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 (var 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;
};
}
// TODO: clear somehow the cache by changing of user permissions
function getUserGroups(options, callback) {
if (that.users[options.user]) {
options.groups = that.users[options.user];
return callback(options);
}
options.groups = [];
that.getForeignObjects('*', 'group', function (err, groups) {
// aggregate all groups permissions, where this user is
if (groups) {
for (var g in groups) {
if (groups[g] &&
groups[g].common &&
groups[g].common.members &&
groups[g].common.members.indexOf(options.user) !== -1) {
options.groups.push(groups[g]._id);
}
}
}
that.users[options.user] = options.groups;
callback(options);
});
}
function checkStates(ids, options, command, callback) {
if (!options.groups) {
return getUserGroups(options, function () {
checkStates(ids, options, command, callback);
});
}
if (ids instanceof Array) {
var errors = [];
var count = ids.length;
if (count === 0) {
callback(null, ids);
return;
}
for (var i = 0; i < ids.length; i++) {
checkStates(ids[i], options, command, function (err, obj) {
if (err && obj) {
errors.push(obj._id);
}
if (!--count) {
if (errors.length) {
for (var j = ids.length - 1; j >= 0; j--) {
if (errors.indexOf(ids[j]) !== -1) {
ids.splice(j, 1);
}
}
}
callback(null, ids);
}
});
}
} else {
var originalChecked = undefined;
if (options.checked !== undefined) originalChecked = options.checked;
options.checked = true;
that.objects.getObject(ids, options, function (err, obj) {
if (originalChecked !== undefined) {
options.checked = originalChecked;
} else {
options.checked = undefined;
}
if (err) {
callback(err, {_id: ids});
return;
} else {
var limitToOwnerRights = options.limitToOwnerRights === true;
if (obj && obj.acl) {
if (obj.acl.state === undefined) obj.acl.state = obj.acl.object;
if (obj.acl.state !== undefined) {
// If user is owner
if (options.user === obj.acl.owner) {
if (command === 'setState' || command === 'delState') {
if (!(obj.acl.state & (2 << 8))/*write*/) {
that.log.warn('Permission error for user "' + options.user + '": ' + command);
callback('permissionError', {_id: ids});
return;
}
} else if (command === 'getState') {
if (!(obj.acl.state & (4 << 8))/*read*/) {
that.log.warn('Permission error for user "' + options.user + '": ' + command);
callback('permissionError', {_id: ids});
return;
}
} else {
that.log.warn('Called unknown command:' + command);
}
} else if (options.groups.indexOf(obj.acl.ownerGroup) !== -1 && !limitToOwnerRights) {
if (command === 'setState' || command === 'delState') {
if (!(obj.acl.state & (2 << 4))/*write*/) {
that.log.warn('Permission error for user "' + options.user + '": ' + command);
callback('permissionError', {_id: ids});
return;
}
} else if (command === 'getState') {
if (!(obj.acl.state & (4 << 4))/*read*/) {
that.log.warn('Permission error for user "' + options.user + '": ' + command);
callback('permissionError', {_id: ids});
return;
}
} else {
that.log.warn('Called unknown command:' + command);
}
} else if (!limitToOwnerRights) {
if (command === 'setState' || command === 'delState') {
if (!(obj.acl.state & 2)/*write*/) {
that.log.warn('Permission error for user "' + options.user + '": ' + command);
callback('permissionError', {_id: ids});
return;
}
} else if (command === 'getState') {
if (!(obj.acl.state & 4)/*read*/) {
that.log.warn('Permission error for user "' + options.user + '": ' + command);
callback('permissionError', {_id: ids});
return;
}
} else {
that.log.warn('Called unknown command:' + command);
callback('permissionError', {_id: ids});
return;
}
}
else {
that.log.warn('Permissions limited to Owner rights');
callback('permissionError', {_id: ids});
return;
}
}
else if (limitToOwnerRights) {
that.log.warn('Permissions limited to Owner rights');
callback('permissionError', {_id: ids});
return;
}
}
else if (limitToOwnerRights){
that.log.warn('Permissions limited to Owner rights');
callback('permissionError', {_id: ids});
return;
}
}
callback();
});
}
}
// find out default history instance
function getDefaultHistory(callback) {
if (!that.defaultHistory) {
// read default history instance from system.config
return that.getForeignObject('system.config', function (err, data) {
if (data && data.common) that.defaultHistory = data.common.defaultHistory;
// if no default history set
if (!that.defaultHistory) {
// read all adapters
that.objects.getObjectView('system', 'instance', {startkey: '', endkey: '\u9999'}, function (err, _obj) {
if (_obj) {
for (var i = 0; i < _obj.rows.length; i++) {
if (_obj.rows[i].value.common && _obj.rows[i].value.common.type === 'storage') {
that.defaultHistory = _obj.rows[i].id.substring('system.adapter.'.length);
break;
}
}
}
if (!that.defaultHistory) that.defaultHistory = 'history.0';
if (callback) callback();
});
} else {
if (callback) callback();
}
});
} else {
if (callback) callback();
}
}
function pattern2RegEx(pattern) {
if (pattern !== '*') {
if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$';
if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern;
}
pattern = (pattern || '').toString().replace(/\./g, '\\.');
pattern = pattern.replace(/\*/g, '.*');
return pattern;
}
function _setStateChangedHelper (id, state, callback) {
that.getForeignState(id, function (err, oldState) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
var 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 (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;
}
if (differ) {
that.outputCount++;
that.states.setState(id, state, function (/* err */) {
if (typeof callback === 'function') callback(null, id, false);
});
} else {
if (typeof callback === 'function') callback(null, id, true);
}
}
});
}
// initStates is called from initAdapter
function initStates(cb) {
logger.debug(that.namespace + ' objectDB connected');
config.states.maxQueue = config.states.maxQueue || 1000;
// Internal object, but some special adapters want to access it anyway.
that.states = new States({
namespace: that.namespace,
connection: config.states,
connected: function () {
logger.debug(that.namespace + ' statesDB connected');
if (options.subscribable) {
that.states.subscribe('system.adapter.' + that.namespace + '.subscribes');
that.states.getState('system.adapter.' + that.namespace + '.subscribes', function (err, state) {
if (!state || !state.val) {
that.patterns = {};
} else {
try {
that.patterns = JSON.parse(state.val);
for (var p in that.patterns) {
that.patterns[p].regex = pattern2RegEx(p);
}
} catch (e) {
that.patterns = {};
}
}
if (typeof cb === 'function') cb();
});
} else if (typeof cb === 'function') {
cb();
}
},
logger: logger,
change: function (id, state) {
that.inputCount++;
if (state === 'null') state = null;
if (!id || typeof id !== 'string') {
console.log('Something is wrong! ' + JSON.stringify(id));
return;
}
// todo remove it as an error with log will be found
if (id === 'system.adapter.' + that.namespace + '.checkLogging') {
checkLogging();
return;
}
// someone subscribes or unsubscribes from adapter
if (options.subscribable && id === 'system.adapter.' + that.namespace + '.subscribes') {
var subs;
try {
subs = JSON.parse(state.val || '{}');
} catch (e) {
subs = {};
}
for (var p in subs) {
subs[p].regex = pattern2RegEx(p);
}
that.patterns = subs;
if (typeof options.subscribesChange === 'function') {
options.subscribesChange(state);
} else {
that.emit('subscribesChange', state);
}
return;
}
// Clear cache if accidentally got the message about change (Will work for admin and javascript)
if (id.match(/^system\.user\./) || id.match(/^system\.group\./)) {
that.users = [];
}
// If someone want to have log messages
if (that.logList && id.match(/\.logging$/)) {
var instance = id.substring(0, id.length - '.logging'.length);
if (logger) logger.debug(that.namespace + ' ' + instance + ': logging ' + (state ? state.val : false));
that.logRedirect(state ? state.val : false, instance);
} else
if (id === 'log.system.adapter.' + that.namespace) {
that.processLog(state);
} else
// If this is messagebox
if (id === 'messagebox.system.adapter.' + that.namespace && state) {
// Read it from fifo list
that.states.delMessage('system.adapter.' + that.namespace, state._id);
var obj = state;
if (obj) {
// If callback stored for this request
if (obj.callback &&
obj.callback.ack &&
obj.callback.id &&
that.callbacks &&
that.callbacks['_' + obj.callback.id]) {
// Call callback function
if (that.callbacks['_' + obj.callback.id].cb) {
that.callbacks['_' + obj.callback.id].cb(obj.message);
delete that.callbacks['_' + obj.callback.id];
}
// delete too old callbacks IDs, like garbage collector
var now = (new Date()).getTime();
for (var _id in that.callbacks) {
if (now - that.callbacks[_id].time > 3600000) delete that.callbacks[_id];
}
} else {
if (options.message) {
// Else inform about new message the adapter
options.message(obj);
}
that.emit('message', obj);
}
}
} else {
if (that.oStates) {
if (!state) {
delete that.oStates[id];
} else {
that.oStates[id] = state;
}
}
// It was an error in the calculation
if ((options.noNamespace || config.noNamespace) && that._namespaceRegExp.test(id)) {
if (typeof options.stateChange === 'function') {
options.stateChange(id.substring(that.namespace.length + 1), state);
} else {
// emit 'stateChange' event instantly
setImmediate(function () {
that.emit('stateChange', id.slice(that.namespace.length + 1), state);
});
}
} else {
if (typeof options.stateChange === 'function') {
options.stateChange(id, state);
} else {
// emit 'stateChange' event instantly
setImmediate(function () {
that.emit('stateChange', id, state);
});
}
}
}
},
connectTimeout: function (error) {
if (config.isInstall) {
if (logger) logger.warn(that.namespace + ' no connection to states DB');
process.exit(0);
} else {
if (logger) logger.error(that.namespace + ' no connection to states DB: ' + (error || ''));
}
}
});
/**
* 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} 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>
*/
that.sendTo = function sendTo(instanceName, command, message, callback) {
if ((typeof message === 'function') && (typeof callback === 'undefined')) {
callback = message;
message = undefined;
}
if (typeof message === 'undefined') {
message = command;
command = 'send';
}
var obj = {command: command, message: message, from: 'system.adapter.' + that.namespace};
if (!instanceName.match(/^system\.adapter\./)) instanceName = 'system.adapter.' + instanceName;
if (typeof message !== 'object') {
that.log.debug('sendTo "' + command + '" to ' + instanceName + ' from system.adapter.' + that.namespace + ': ' + message);
} else {
that.log.debug('sendTo "' + command + '" to ' + instanceName + ' from system.adapter.' + that.namespace);
}
// If not specific instance
if (!instanceName.match(/\.[0-9]+$/)) {
// Send to all instances of adapter
that.objects.getObjectView('system', 'instance', {startkey: instanceName + '.', endkey: instanceName + '.\u9999'}, function (err, _obj) {
if (_obj) {
for (var i = 0; i < _obj.rows.length; i++) {
that.states.pushMessage(_obj.rows[i].id, obj);
}
}
});
} else {
if (callback) {
if (typeof callback === 'function') {
// force subscribe even no messagebox enabled
if (!that.common.messagebox && !that.mboxSubscribed) {
that.mboxSubscribed = true;
that.states.subscribeMessage('system.adapter.' + that.namespace);
}
obj.callback = {
message: message,
id: callbackId++,
ack: false,
time: (new Date()).getTime()
};
if (callbackId >= 0xFFFFFFFF) callbackId = 1;
if (!that.callbacks) that.callbacks = {};
that.callbacks['_' + obj.callback.id] = {cb: callback};
// delete too old callbacks IDs
var now = (new Date()).getTime();
for (var _id in that.callbacks) {
if (now - that.callbacks[_id].time > 3600000) delete that.callbacks[_id];
}
} else {
obj.callback = callback;
obj.callback.ack = true;
}
}
that.states.pushMessage(instanceName, obj);
}
};
/**
* Promise-version of Adapter.sendTo
*/
that.sendToAsync = tools.promisifyNoError(that.sendTo, that);
/**
* 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 {string} 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} 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>
*/
that.sendToHost = function sendToHost(hostName, command, message, callback) {
if (typeof message === 'undefined') {
message = command;
command = 'send';
}
var obj = {command: command, message: message, from: 'system.adapter.' + that.namespace};
if (hostName && !hostName.match(/^system\.host\./)) hostName = 'system.host.' + hostName;
if (!hostName) {
// Send to all hosts
that.objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.' + '\u9999'}, null, function (err, res) {
if (!err && res.rows.length) {
for (var i = 0; i < res.rows.length; i++) {
var parts = res.rows[i].id.split('.');
// ignore system.host.name.alive and so on
if (parts.length === 3) {
that.states.pushMessage(res.rows[i].id, obj);
}
}
}
});
} else {
if (callback) {
if (typeof callback === 'function') {
// force subscribe even no messagebox enabled
if (!that.common.messagebox && !that.mboxSubscribed) {
that.mboxSubscribed = true;
that.states.subscribeMessage('system.adapter.' + that.namespace);
}
obj.callback = {
message: message,
id: callbackId++,
ack: false,
time: (new Date()).getTime()
};
if (callbackId >= 0xFFFFFFFF) callbackId = 1;
if (!that.callbacks) that.callbacks = {};
that.callbacks['_' + obj.callback.id] = {cb: callback};
} else {
obj.callback = callback;
obj.callback.ack = true;
}
}
that.states.pushMessage(hostName, obj);
}
};
/**
* Promise-version of Adapter.sendToHost
*/
that.sendToHostAsync = tools.promisifyNoError(that.sendToHost, that);
/**
* 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
* }
* </code></pre>
* @param {boolean} ack optional is command(false) or status(true)
* @param {object} options optional user context
* @param {function} callback optional return error and id
* <pre><code>
* function (err, id) {
* if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
* }
* </code></pre>
*/
that.setState = function setState(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 = {};
}
id = that._fixId(id, false, 'state');
if (typeof ack === 'function') {
callback = ack;
ack = undefined;
}
if (typeof state !== 'object' || state === null || state === undefined) state = {val: state};
if (ack !== undefined) {
state.ack = ack;
}
state.from = 'system.adapter.' + that.namespace;
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'setState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
that.outputCount++;
that.states.setState(id, state, callback);
}
});
} else {
that.outputCount++;
that.states.setState(id, state, callback);
}
};
/**
* Promise-version of Adapter.setState
*/
that.setStateAsync = tools.promisify(that.setState, that);
/**
* 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 {function} callback optional return error and id
* <pre><code>
* function (err, id, notChanged) {
* if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
* if (!notChanged) adapter.log.debug('Value was chnaged');
* }
* </code></pre>
*/
that.setStateChanged = function 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 = {};
}
id = that._fixId(id, false, 'state');
if (typeof ack === 'function') {
callback = ack;
ack = undefined;
}
if (typeof state !== 'object' || state === null || state === undefined) state = {val: state};
if (ack !== undefined) {
state.ack = ack;
}
state.from = 'system.adapter.' + that.namespace;
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'setState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
_setStateChangedHelper(id, state, callback);
}
});
} else {
_setStateChangedHelper(id, state, callback);
}
};
/**
* Promise-version of Adapter.setStateChanged
*/
that.setStateChangedAsync = tools.promisify(that.setStateChanged, that, ["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
* }
* </code></pre>
* @param {boolean} ack optional is command(false) or status(true)
* @param {object} options optional user context
* @param {function} callback optional return error and id
* <pre><code>
* function (err, id) {
* if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
* }
* </code></pre>
*/
that.setForeignState = function setForeignState(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 (typeof state !== 'object' || state === null || state === undefined) state = {val: state};
if (ack !== undefined) {
state.ack = ack;
}
state.from = 'system.adapter.' + that.namespace;
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'setState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
that.outputCount++;
that.states.setState(id, state, callback);
}
});
} else {
that.outputCount++;
that.states.setState(id, state, callback);
}
};
/**
* Promise-version of Adapter.setForeignState
*/
that.setForeignStateAsync = tools.promisify(that.setForeignState, that);
/**
* 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
* }
* </code></pre>
* @param {boolean} ack optional is command(false) or status(true)
* @param {object} options optional user context
* @param {function} callback optional return error and id
* <pre><code>
* function (err, id) {
* if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
* }
* </code></pre>
*/
that.setForeignStateChanged = function 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 (typeof state !== 'object' || state === null || state === undefined) state = {val: state};
if (ack !== undefined) {
state.ack = ack;
}
state.from = 'system.adapter.' + that.namespace;
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'setState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
_setStateChangedHelper(id, state, callback);
}
});
} else {
_setStateChangedHelper(id, state, callback);
}
};
/**
* Promise-version of Adapter.setForeignStateChanged
*/
that.setForeignStateChangedAsync = tools.promisify(that.setForeignStateChanged, that);
/**
* 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 {function} 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
*/
that.getState = function getState(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
id = that._fixId(id, false, 'state');
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'getState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
if (that.oStates && that.oStates[id]) {
if (typeof callback === 'function') callback(null, that.oStates[id]);
} else {
that.states.getState(id, callback);
}
}
});
} else {
if (that.oStates && that.oStates[id]) {
if (typeof callback === 'function') callback(null, that.oStates[id]);
} else {
that.states.getState(id, callback);
}
}
};
/**
* Promise-version of Adapter.getState
*/
that.getStateAsync = tools.promisify(that.getState, that);
/**
* 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 {function} 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
*/
that.getForeignState = function getForeignState(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'getState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
if (that.oStates && that.oStates[id]) {
if (typeof callback === 'function') callback(null, that.oStates[id]);
} else {
that.states.getState(id, callback);
}
}
});
} else {
if (that.oStates && that.oStates[id]) {
if (typeof callback === 'function') callback(null, that.oStates[id]);
} else {
that.states.getState(id, callback);
}
}
};
/**
* Promise-version of Adapter.getForeignState
*/
that.getForeignStateAsync = tools.promisify(that.getForeignState, that);
/**
* 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 - new Date().getTime()', by default is (now - 1 week)
* - end - (optional) time in ms - new Date().getTime()', 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 {function} 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
*/
that.getHistory = function getHistory(id, options, callback) {
options = options || {};
options.end = options.end || (new Date()).getTime() + 5000000;
if (!options.count && !options.start) {
options.start = options.start || (new Date()).getTime() - 604800000; // - 1 week
}
if (!options.instance) {
if (!that.defaultHistory) {
// read default history instance from system.config
return getDefaultHistory(function () {
that.getHistory(id, options, callback);
});
} else {
options.instance = that.defaultHistory;
}
}
that.sendTo(options.instance || 'history.0', 'getHistory', {id: id, options: options}, function (res) {
setImmediate(function () {
callback(res.error, res.result, res.step, res.sessionId);
});
});
};
/**
* Promise-version of Adapter.getHistory
*/
that.getHistoryAsync = tools.promisify(that.getHistory, that, ["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
*/
that.idToDCS = function idToDCS(id) {
if (!id) return null;
var parts = id.split('.');
if (parts[0] + '.' + parts[1] !== that.namespace) {
that.log.warn('Try to decode id not from this adapter');
return null;
}
return {device: parts[2], channel: parts[3], state: parts[4]};
};
/**
* Delete one state of this adapter.
*
* Deletes the state. If State does not exist, no error will be returned.
* Do not forget do delete the object for the state too (with delObject)
* <pre><code>
* adapter.delState('stateID', function (err) {
* console.log('adapterName.0.stateID is deleted');
* });
* </code></pre>
*
* @alias delState
* @memberof Adapter
* @param {string} id short or long string of ID like "stateID" or "adapterName.0.stateID".
* @param {object} options optional argument to describe the user context
* @param {function} callback return result function (err) {}
*/
that.delState = function delState(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
id = that._fixId(id, false, 'state');
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'delState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
that.states.delState(id, callback);
}
});
} else {
that.states.delState(id, callback);
}
};
/**
* Promise-version of Adapter.delState
*/
that.delStateAsync = tools.promisify(that.delState, that);
/**
* Delete one state of any adapter.
*
* Deletes the state. If State does not exist, no error will be returned.
* Do not forget do delete the object for the state too (with delObject)
* <pre><code>
* adapter.delState('adapterName.0.stateID', function (err) {
* console.log('adapterName.0.stateID is deleted');
* });
* </code></pre>
*
* @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 {function} callback return result function (err) {}
*/
that.delForeignState = function delForeignState(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'delState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
that.states.delState(id, callback);
}
});
} else {
that.states.delState(id, callback);
}
};
/**
* Promise-version of Adapter.delForeignState
*/
that.delForeignStateAsync = tools.promisify(that.delForeignState, that);
/**
* 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 {function} callback return result function (err, states) {}, where states is an object like {"ID1": {"val": 1, "ack": true}, "ID2": {"val": 2, "ack": false}, ...}
*/
that.getStates = function getStates(pattern, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
pattern = that._fixId(pattern, true, 'state');
that.getForeignStates(pattern, options, callback);
};
/**
* Promise-version of Adapter.getStates
*/
that.getStatesAsync = tools.promisify(that.getStates, that);
/**
* 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} 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 {function} callback return result function (err, states) {}, where states is an object like {"ID1": {"val": 1, "ack": true}, "ID2": {"val": 2, "ack": false}, ...}
*/
that.getForeignStates = function getForeignStates(pattern, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
var list = {};
if (typeof pattern === 'function') {
callback = pattern;
pattern = '*';
}
if (typeof callback !== 'function') {
logger.error(that.namespace + ' getForeignStates invalid callback for ' + pattern);
return;
}
if (typeof pattern === 'object') {
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(pattern, options, 'getState', function (err, keys) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
that.states.getStates(keys, function (err, arr) {
if (err) {
callback(err);
return;
}
for (var i = 0; i < keys.length; i++) {
if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]);
list[keys[i]] = arr[i] || {};
}
callback(null, list);
});
});
} else {
that.states.getStates(pattern, function (err, arr) {
if (err) {
callback(err);
return;
}
for (var i = 0; i < pattern.length; i++) {
if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]);
list[pattern[i]] = arr[i] || {};
}
callback(null, list);
});
}
return;
}
var keys = [];
var params = {};
if (pattern && pattern !== '*') {
params = {
startkey: pattern.replace('*', ''),
endkey: pattern.replace('*', '\u9999')
};
}
var originalChecked = undefined;
if (options.checked !== undefined) originalChecked = options.checked;
options.checked = true;
that.objects.getObjectView('system', 'state', params, options, function (err, res) {
if (originalChecked !== undefined) {
options.checked = originalChecked;
} else {
options.checked = undefined;
}
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
// filter out
var regEx;
// process patterns like "*.someValue". The patterns "someValue.*" will be processed by getObjectView
if (pattern && pattern !== '*' && pattern[pattern.length - 1] !== '*') {
regEx = new RegExp(pattern2RegEx(pattern));
}
for (var i = 0; i < res.rows.length; i++) {
if (!regEx || regEx.test(res.rows[i].id)) {
keys.push(res.rows[i].id);
}
}
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(keys, options, 'getState', function (err, keys) {
if (err) {
if (typeof callback === 'function') callback(err);
return;
}
that.states.getStates(keys, function (err, arr) {
if (err) {
callback(err);
return;
}
for (var i = 0; i < res.rows.length; i++) {
if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]);
list[keys[i]] = arr[i] || null;
}
if (typeof callback === 'function') callback(null, list);
});
});
} else {
that.states.getStates(keys, function (err, arr) {
if (err) {
callback(err);
return;
}
for (var i = 0; i < res.rows.length; i++) {
if (typeof arr[i] === 'string') {
try {
arr[i] = JSON.parse(arr[i]);
} catch (e) {
logger.error(that.namespace + ' Cannot parse state"' + keys[i] + ': ' + arr[i]);
}
}
list[keys[i]] = arr[i] || null;
}
if (typeof callback === 'function') callback(null, list);
});
}
});
};
/**
* Promise-version of Adapter.getForeignStates
*/
that.getForeignStatesAsync = tools.promisify(that.getForeignStates, that);
/**
* 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} 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 {function} callback return result function (err) {}
*/
that.subscribeForeignStates = function subscribeForeignStates(pattern, options, callback) {
if (!pattern) pattern = '*';
if (typeof options === 'function') {
callback = options;
options = null;
}
// Todo check rights for options
autoSubscribeOn(function () {
// compare if this pattern for one of autosubscribe adapters
for (var s = 0; s < that.autoSubscribe.length; s++) {
if (pattern === '*' || pattern.substring(0, that.autoSubscribe[s].length + 1) === that.autoSubscribe[s] + '.') {
// put this pattern into adapter list
that.states.getState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', function (err, state) {
state = {};
state.val = state.val || '{}';
var subs;
try {
subs = JSON.parse(state.val);
} catch (e) {
that.log.error('Cannot parse subscribes for "' + that.autoSubscribe[s] + '.subscribes"');
}
subs[pattern] = subs[pattern] || {};
subs[pattern][that.namespace] = subs[pattern][that.namespace] || 0;
subs[pattern][that.namespace]++;
that.outputCount++;
that.states.setState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', subs);
});
}
}
that.states.subscribe(pattern, callback);
});
};
/**
* Promise-version of Adapter.subscribeForeignStates
*/
that.subscribeForeignStatesAsync = tools.promisify(that.subscribeForeignStates, that);
/**
* Unsubscribe for changes for given pattern
*
* This function allows to unsubsrcibe 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} pattern string in form 'adapter.0.*'. Must be the same as subscribe.
* @param {object} options optional argument to describe the user context
* @param {function} callback return result function (err) {}
*/
that.unsubscribeForeignStates = function unsubscribeForeignStates(pattern, options, callback) {
if (!pattern) pattern = '*';
// Todo check rights for options
if (typeof options === 'function') {
callback = options;
options = null;
}
if (that.autoSubscribe) {
for (var s = 0; s < that.autoSubscribe.length; s++) {
if (pattern === '*' || pattern.substring(0, that.autoSubscribe[s].length + 1) === that.autoSubscribe[s] + '.') {
// remove this pattern from adapter list
that.states.getState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', function (err, state) {
if (!state || !state.val) return;
var subs;
try {
subs = JSON.parse(state.val);
} catch (e) {
that.log.error('Cannot parse subscribes for "' + that.autoSubscribe[s] + '.subscribes"');
return;
}
if (!subs[pattern]) return;
if (subs[pattern][that.namespace] === undefined) return;
subs[pattern][that.namespace]--;
if (subs[pattern][that.namespace] <= 0) delete subs[pattern][that.namespace];
var found = false;
// if any other subs are there
for (var id in subs[pattern]) {
if (subs[pattern].hasOwnProperty(id)) {
found = true;
break;
}
}
if (!found) delete subs[pattern];
that.outputCount++;
that.states.setState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', subs);
});
}
}
}
that.states.unsubscribe(pattern, callback);
};
/**
* Promise-version of Adapter.unsubscribeForeignStates
*/
that.unsubscribeForeignStatesAsync = tools.promisify(that.unsubscribeForeignStates, that);
/**
* 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. It can be array of IDs too.
* @param {object} options optional argument to describe the user context
* @param {function} callback return result function (err) {}
*/
that.subscribeStates = function subscribeStates(pattern, options, callback) {
// Todo check rights for options
if (typeof options === 'function') {
callback = options;
options = null;
}
// Exception. Threat the '*' case automatically
if (!pattern || pattern === '*') {
that.states.subscribe(that.namespace + '.*', callback);
} else {
pattern = that._fixId(pattern, true, 'state');
that.states.subscribe(pattern, callback);
}
};
/**
* Promise-version of Adapter.subscribeStates
*/
that.subscribeStatesAsync = tools.promisify(that.subscribeStates, that);
/**
* Unsubscribe for changes for given pattern for own states.
*
* This function allows to unsubsrcibe 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 {function} callback return result function (err) {}
*/
that.unsubscribeStates = function unsubscribeStates(pattern, options, callback) {
// Todo check rights for options
if (typeof options === 'function') {
callback = options;
options = null;
}
if (!pattern || pattern === '*') {
that.states.unsubscribe(that.namespace + '.*', callback);
} else {
pattern = that._fixId(pattern, true, 'state');
that.states.unsubscribe(pattern, callback);
}
};
/**
* Promise-version of Adapter.unsubscribeStates
*/
that.unsubscribeStatesAsync = tools.promisify(that.unsubscribeStates, that);
that.pushFifo = function pushFifo(id, state, callback) {
that.states.pushFifo(id, state, callback);
};
that.trimFifo = function trimFifo(id, start, end, callback) {
that.states.trimFifo(id, start, end, callback);
};
that.getFifoRange = function getFifoRange(id, start, end, callback) {
that.states.getFifoRange(id, start, end, callback);
};
that.getFifo = function getFifo(id, callback) {
that.states.getFifo(id, callback);
};
that.lenFifo = function lenFifo(id, callback) {
that.states.lenFifo(id, callback);
};
that.subscribeFifo = function subscribeFifo(pattern) {
that.states.subscribeFifo(pattern);
};
that.getSession = function getSession(id, callback) {
that.states.getSession(id, callback);
};
that.setSession = function setSession(id, ttl, data, callback) {
that.states.setSession(id, ttl, data, callback);
};
that.destroySession = function destroySession(id, callback) {
that.states.destroySession(id, callback);
};
that.getMessage = function getMessage(callback) {
that.states.getMessage('system.adapter.' + that.namespace, callback);
};
that.lenMessage = function lenMessage(callback) {
that.states.lenMessage('system.adapter.' + that.namespace, callback);
};
// Write binary block into redis, e.g image
that.setBinaryState = function setBinaryState(id, binary, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'setState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
that.states.setBinaryState(id, binary, callback);
}
});
} else {
that.states.setBinaryState(id, binary, callback);
}
};
/**
* Promise-version of Adapter.setBinaryState
*/
that.setBinaryStateAsync = tools.promisify(that.setBinaryState, that);
// Read binary block from redis, e.g. image
that.getBinaryState = function getBinaryState(id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (options && options.user && options.user !== 'system.user.admin') {
checkStates(id, options, 'getState', function (err) {
if (err) {
if (typeof callback === 'function') callback(err);
} else {
that.states.getBinaryState(id, callback);
}
});
} else {
that.states.getBinaryState(id, callback);
}
};
/**
* Promise-version of Adapter.getBinaryState
*/
that.getBinaryStateAsync = tools.promisify(that.getBinaryState, that);
}
// read all logs prepared for this adapter at start
function readLogs(callback) {
// read all stored messages
that.states.getLog('system.adapter.' + that.namespace, function (err, msg) {
if (msg) {
if (msg) that.emit('log', msg);
setImmediate(function () {
readLogs(callback);
});
} else if (callback) {
callback();
}
});
}
function printLog(logs, id, callback) {
that.states.lenLog(id, function (err, len) {
logs.push('Subscriber - ' + id + ' (queued ' + len + ') ' + (err || ''));
if (callback) callback();
});
}
// debug function to find error with stop logging
function checkLogging() {
var logs = [];
var count = 0;
// LogList
logs.push('Actual Loglist - ' + JSON.stringify(that.logList));
// Read current state of all log subscribers
that.states.getKeys('*.logging', function (err, keys) {
if (keys && keys.length) {
that.states.getStates(keys, function (err, obj) {
if (obj) {
for (var i = 0; i < keys.length; i++) {
// We can JSON.parse, but index is 16x faster
if (obj[i]) {
var id = keys[i].substring(0, keys[i].length - '.logging'.length).replace(/^io\./, '');
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'))) {
count++;
printLog(logs, id, function () {
if (!--count) {
for (var m = 0; m < logs.length; m++) {
that.log.error('LOGINFO: ' + logs[m]);
}
logs = null;
}
});
} else {
if (logs) {
logs.push('Subscriber - ' + id + ' (disabled)');
} else {
that.log.error('LOGINFO: Subscriber - ' + id + ' (disabled)');
}
}
}
}
}
if (!count && logs) {
for (var m = 0; m < logs.length; m++) {
that.log.error('LOGINFO: ' + logs[m]);
}
logs = null;
}
});
}
});
}
function initLogging() {
// temporary log buffer
var messages = [];
// Read current state of all log subscriber
that.states.getKeys('*.logging', function (err, keys) {
if (keys && keys.length) {
that.states.getStates(keys, function (err, obj) {
if (obj) {
for (var i = 0; i < keys.length; i++) {
// We can JSON.parse, but index is 16x faster
if (!obj[i]) continue;
var 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)) {
that.logRedirect(true, id);
} else if (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true')) {
that.logRedirect(true, id);
}
}
if (that.logList.length && messages && messages.length) {
for (var m = 0; m < messages.length; m++) {
for (var k = 0; k < that.logList.length; k++) {
that.states.pushLog(that.logList[k], messages[m]);
}
}
}
}
// clear log buffer
messages = null;
});
} else {
// disable log buffer
messages = null;
}
});
that.logRedirect = function (isActive, id) {
// ignore itself
if (id === 'system.adapter.' + that.namespace) return;
if (isActive) {
if (that.logList.indexOf(id) === -1) that.logList.push(id);
} else {
var pos = that.logList.indexOf(id);
if (pos !== -1) that.logList.splice(pos, 1);
}
};
// If some message from logger
logger.on('logging', function (transport, level, msg /* , meta */) {
if (transport.name !== tools.appName) return;
var message = {message: msg, severity: level, from: that.namespace, ts: (new Date()).getTime()};
if (options.logTransporter) {
that.emit('log', message);
}
if (!that.logList.length) {
// if log buffer still active
if (messages && !options.logTransporter) {
messages.push(message);
// do not let messages to grow without limit
if (messages.length > config.states.maxQueue) {
messages.splice(0, messages.length - config.states.maxQueue);
}
}
} else {
// Send to all adapter, that required logs
for (var i = 0; i < that.logList.length; i++) {
that.states.pushLog(that.logList[i], message);
}
}
});
options.logTransporter = options.logTransporter || that.ioPack.common.logTransporter;
if (options.logTransporter) {
that.requireLog = function (isActive) {
if (that.states) {
if (that.logRequired !== isActive) {
that.logRequired = isActive; // remember state
if (!isActive) {
if (that.logOffTimer) {
clearTimeout(that.logOffTimer);
}
// disable log receiving after 10 seconds
that.logOffTimer = setTimeout(function () {
that.logOffTimer = null;
that.log.debug('Change log subscriber state: FALSE');
that.outputCount++;
that.states.setState('system.adapter.' + that.namespace + '.logging', {val: false, ack: true, from: 'system.adapter.' + that.namespace});
}, 10000);
} else {
if (that.logOffTimer) {
clearTimeout(that.logOffTimer);
that.logOffTimer = null;
} else {
that.log.debug('Change log subscriber state: true');
that.outputCount++;
that.states.setState('system.adapter.' + that.namespace + '.logging', {val: true, ack: true, from: 'system.adapter.' + that.namespace});
}
}
}
}
};
that.processLog = function (msg) {
if (msg) that.emit('log', msg);
that.states.delLog('system.adapter.' + that.namespace, msg._id);
};
readLogs();
that.states.subscribeLog('system.adapter.' + that.namespace);
}
}
function initAdapter(adapterConfig) {
initLogging();
if (options.instance === undefined) {
if (!adapterConfig || !adapterConfig.common || !adapterConfig.common.enabled) {
if (adapterConfig && adapterConfig.common && adapterConfig.common.enabled !== undefined) {
if (!config.isInstall) logger.error(that.namespace + ' adapter disabled');
} else {
if (!config.isInstall) logger.error(that.namespace + ' no config found for adapter');
}
if (!config.isInstall && (!process.argv || !config.forceIfDisabled)) {
var id = 'system.adapter.' + that.namespace;
that.outputCount += 2;
that.states.setState(id + '.alive', {val: true, ack: true, expire: 30, from: id});
that.states.setState(id + '.connected', {val: true, ack: true, expire: 30, from: id}, function () {
process.exit(3);
});
setTimeout(function () {
process.exit(3);
}, 1000);
return;
}
}
if (!config.isInstall && !adapterConfig._id) {
logger.error(that.namespace + ' invalid config: no _id found');
process.exit(4);
return;
}
var name;
var instance;
if (!config.isInstall) {
var tmp = adapterConfig._id.match(/^system\.adapter\.([a-zA-Z0-9-_]+)\.([0-9]+)$/);
if (!tmp) {
logger.error(that.namespace + ' invalid config');
process.exit(5);
return;
}
name = tmp[1];
instance = parseInt(tmp[2]) || 0;
} else {
name = options.name;
instance = 0;
adapterConfig = adapterConfig || {common: {mode: 'once', name: name}, native: {}};
}
for (var tp in logger.transports) {
if (logger.transports.hasOwnProperty(tp)) {
logger.transports[tp].level = adapterConfig.common.logLevel || 'info';
}
}
that.name = adapterConfig.common.name;
that.instance = instance;
that.namespace = name + '.' + instance;
process.title = 'io.' + that.namespace;
that.config = adapterConfig.native;
that.host = adapterConfig.common.host;
that.common = adapterConfig.common;
if (adapterConfig.common.mode === 'subscribe' ||
adapterConfig.common.mode === 'schedule' ||
adapterConfig.common.mode === 'once') {
that.stop = function () {
stop(true);
};
}
// Monitor logging state
that.states.subscribe('*.logging');
if (typeof options.message === 'function' && !adapterConfig.common.messagebox) {
logger.error(that.namespace + ' : 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) {
that.mboxSubscribed = true;
that.states.subscribeMessage('system.adapter.' + that.namespace);
}
// set configured in DB log level
if (adapterConfig.common.loglevel) {
for (var trans in logger.transports) {
if (logger.transports.hasOwnProperty(trans)) {
logger.transports[trans].level = adapterConfig.common.loglevel;
}
}
}
} else {
that.name = adapterConfig.name || options.name;
that.instance = adapterConfig.instance || 0;
that.namespace = that.name + '.' + that.instance;
that.config = adapterConfig.native || {};
that.common = adapterConfig.common || {};
that.host = that.common.host || tools.getHostName() || require('os').hostname();
}
var Log = function () {};
if (config.consoleOutput) {
Log.prototype.silly = function (msg) {
console.log(msg);
logger.silly(that.namespace + ' ' + msg);
};
Log.prototype.debug = function (msg) {
console.log(msg);
logger.debug(that.namespace + ' ' + msg);
};
Log.prototype.info = function (msg) {
console.log(msg);
logger.info(that.namespace + ' ' + msg);
};
Log.prototype.error = function (msg) {
console.error(msg);
logger.error(that.namespace + ' ' + msg);
};
Log.prototype.warn = function (msg) {
console.warn(msg);
logger.warn(that.namespace + ' ' + msg);
};
} else {
Log.prototype.silly = function (msg) {
logger.silly(that.namespace + ' ' + msg);
};
Log.prototype.debug = function (msg) {
logger.debug(that.namespace + ' ' + msg);
};
Log.prototype.info = function (msg) {
logger.info(that.namespace + ' ' + msg);
};
Log.prototype.error = function (msg) {
logger.error(that.namespace + ' ' + msg);
};
Log.prototype.warn = function (msg) {
logger.warn(that.namespace + ' ' + msg);
};
}
that.log = new Log();
that.log.level = config.log.level;
if (options.instance === undefined) {
that.version = (that.pack && that.pack.version) ? that.pack.version : ((that.ioPack && that.ioPack.common) ? that.ioPack.common.version : 'unknown');
that.log.info('starting. Version ' + that.version + ' in ' + that.adapterDir + ', node: ' + process.version);
config.system = config.system || {};
config.system.statisticsInterval = parseInt(config.system.statisticsInterval, 10) || 15000;
reportInterval = setInterval(reportStatus, config.system.statisticsInterval);
reportStatus();
}
if (adapterConfig && adapterConfig.common && adapterConfig.common.restartSchedule) {
try {
schedule = require('node-schedule');
} catch (e) {
that.log.error('Cannot load node-schedule. Scheduled restart is disabled');
}
if (schedule) {
that.log.debug('Schedule restart: ' + adapterConfig.common.restartSchedule);
schedule.scheduleJob(adapterConfig.common.restartSchedule, function () {
that.log.info('Scheduled restart.');
stop(false, true);
});
}
}
// auto oStates
if (options.states) {
that.getStates('*', function (err, _states) {
that.oStates = _states;
that.subscribeStates('*');
if (typeof options.ready === 'function') options.ready();
that.emit('ready');
});
} else {
if (typeof options.ready === 'function') options.ready();
that.emit('ready');
// todo remove it later, when the error is fixed
that.subscribeStates('checkLogging');
}
}
function reportStatus() {
var id = 'system.adapter.' + that.namespace;
that.outputCount += 7;
that.states.setState(id + '.alive', {val: true, ack: true, expire: Math.floor(config.system.statisticsInterval / 1000) + 10, from: id});
if (that.connected) {
that.states.setState(id + '.connected', {val: true, ack: true, expire: 30, from: id});
that.outputCount++;
}
//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).
var mem = process.memoryUsage();
that.states.setState(id + '.memRss', {val: parseFloat((mem.rss / 1048576/* 1MB */).toFixed(2)), ack: true, from: id});
that.states.setState(id + '.memHeapTotal', {val: parseFloat((mem.heapTotal / 1048576/* 1MB */).toFixed(2)), ack: true, from: id});
that.states.setState(id + '.memHeapUsed', {val: parseFloat((mem.heapUsed / 1048576/* 1MB */).toFixed(2)), ack: true, from: id});
// Under windows toFixed returns string ?
that.states.setState(id + '.uptime', {val: parseInt(process.uptime().toFixed(), 10), ack: true, from: id});
that.states.setState(id + '.inputCount', {val: that.inputCount, ack: true, from: id});
that.states.setState(id + '.outputCount', {val: that.outputCount, ack: true, from: id});
that.inputCount = 0;
that.outputCount = 0;
}
function stop(isPause, isScheduled) {
clearInterval(reportInterval);
var id = 'system.adapter.' + that.namespace;
if (typeof options.unload === 'function') {
options.unload(function () {
if (that.states) {
that.outputCount++;
that.states.setState(id + '.alive', {val: false, ack: true, from: id}, function () {
if (!isPause && that.log) that.log.info('terminating');
process.exit(isScheduled ? -100: 0);
});
}
});
} else {
that.emit('unload', function () {
if (that.states) {
that.outputCount++;
that.states.setState(id + '.alive', {val: false, ack: true, from: id}, function () {
if (!isPause && that.log) that.log.info('terminating');
process.exit(isScheduled ? -100: 0);
});
}
});
// Make delay to let event 'unload' to be processed
setTimeout(function () {
if (that.states) {
that.outputCount++;
that.states.setState(id + '.alive', {val: false, ack: true, from: id}, function () {
if (!isPause && that.log) that.log.info('terminating');
process.exit(isScheduled ? -100: 0);
});
// Give 2 seconds to write the value
setTimeout(function () {
if (!isPause && that.log) that.log.info('terminating with timeout');
process.exit(isScheduled ? -100: 0);
}, 1000);
} else {
if (!isPause && that.log) that.log.info('terminating');
process.exit(isScheduled ? -100: 0);
}
}, that.common ? that.common.stopTimeout || 500 : 500);
}
}
process.once('SIGINT', stop);
process.once('SIGTERM', stop);
// And the exit event shuts down the child.
process.once('exit', stop);
process.on('uncaughtException', function (err) {
console.error(err);
// catch it on windows
if (that.getPortRunning && err.message === 'listen EADDRINUSE') {
logger.warn(that.namespace + ' Port ' + that.getPortRunning.port + ' is in use. Get next');
setImmediate(function () {
that.getPort(that.getPortRunning.port + 1, that.getPortRunning.callback);
});
return;
}
logger.error(that.namespace + ' uncaught exception: ' + (err.message || err));
if (err.stack) logger.error(that.namespace + ' ' + err.stack);
try {
stop();
setTimeout(function () {
process.exit(6);
}, 1000);
} catch (err) {
logger.error(that.namespace + ' exception by stop: ' + (err.message || err));
}
});
return this;
}
// extend the EventEmitter class using our class
util.inherits(Adapter, EventEmitter);
module.exports = Adapter;