iobroker.nuki-extended
Version:
761 lines (683 loc) • 25.5 kB
JavaScript
'use strict';
const _LOCK = require('../_LOCK.js');
const _OPENER = require('../_OPENER.js');
/**
* Library
* !!Extended with two own functions (readData, convertNode)!!
*
* @description Library of general functions as well as helping functions handling ioBroker
* @author Zefau <https://github.com/Zefau/>
* @license MIT License
* @version 0.28.0
* @date 2020-02-09
*/
class Library {
/**
*
*/
static get CONNECTION() {
return {
node: 'info.connection',
description: 'Adapter Connection Status',
role: 'indicator.connected',
type: 'boolean',
};
}
/**
* Constructor.
*
* @param {object} adapter ioBroker adpater object
* @param options
*/
constructor(adapter, options) {
this._adapter = adapter;
this.options = options || {};
this._nodes = this.options.nodes || {};
this.options.updatesInLog = this.options.updatesInLog || false;
this.options.updatesExceptions = this.options.updatesExceptions || [
'timestamp',
'datetime',
'UTC',
'localtime',
'last_use_date',
'lastSeen',
];
this._STATES = {};
this.subscriptions = [];
this.set({ node: 'info', description: 'Adapter Information', role: 'channel' });
this.set(Library.CONNECTION, false);
}
/**
* Gets a node.
*
* @param {string} node Node identifier
* @param lowerCase
* @returns {object} Node
*/
getNode(node, lowerCase) {
return JSON.parse(
JSON.stringify(
this._nodes[this.clean(node, lowerCase)] ||
this._nodes[this.clean(node.replace(RegExp(/\.\d+\./, 'g'), '.'), lowerCase)] || {
description: '(no description given)',
role: 'state',
type: 'string',
convert: null,
},
),
);
}
/**
* Terminate adapter.
*
* @param {string} [message] Message to display
* @param {boolean} [kill] Whether to kill the adapter (red lights) or not (yellow lights)
* @param {integer} [reason] Reason code for exit
* @returns void
*/
terminate(message, kill, reason) {
this.resetStates();
this.set(Library.CONNECTION, false);
message = message ? message : 'Terminating adapter due to error!';
// yellow lights
if (!kill) {
this._adapter.log.warn(message);
}
// red lights
else if (kill === true) {
this._adapter.log.error(message);
// delay necessary to actually show the error message
setTimeout(
() =>
this._adapter && this._adapter.terminate
? this._adapter.terminate(message, reason || 11)
: process.exit(reason || 11),
2000,
);
}
return false;
}
/**
* Remove specials characters from string.
*
* @param {string} string String to proceed
* @param {boolean} [lowerCase] If String shall be return in lower case
* @param {string} [replacement] Characters shall be replaced with this character
* @param {Array} [characters] Characters to be removed from string
* @returns {string} Cleaned String
*/
clean(string, lowerCase, replacement, characters) {
if (!string && typeof string != 'string') {
return string;
}
characters = characters
? characters
: ['<', '>', ' ', ',', ';', '!', '?', '[', ']', '*', "'", '"', '\\', '&', '^', '$', '(', ')', '/'];
characters.forEach(character => {
string = string.replace(RegExp(`\\${character}`, 'gi'), replacement ? replacement : '');
});
return lowerCase ? string.toLowerCase() : string;
}
/**
* Waits for a specific time before invoking a callback.
*
* @param {number} time Time to wait before invoking the callback
* @param {function} callback Callback to be invoked
* @returns void
*/
wait(time, callback) {
setTimeout(() => callback, time);
}
/**
* Encode a string.
*
* @param {string} key Key used for encoding
* @param {string} string String to encode
* @returns {string} Encoded String
*/
encode(key, string) {
let result = '';
for (let i = 0; i < string.length; i++) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ string.charCodeAt(i));
}
return result;
}
/**
* Decode a string.
*
* @param {string} key Key used for decoding
* @param {string} string String to decode
* @returns {string} Decoded String
*/
decode(key, string) {
return this.encode(key, string);
}
/**
* Get a random key.
*
* @param {integer} length Length of key
* @returns {string} Key
*/
getKey(length) {
length = length || 8;
let key = '';
while (key.length < length) {
key +=
Math.random().toString().substring(2, 3) >= 5
? Math.random().toString(36).substring(2, 4)
: Math.random().toString(36).substring(2, 4).toUpperCase();
}
return key.substr(0, length);
}
/**
* Convert an integer to IP.
*
* @param {integer} number Number to be converted to IP address
* @returns {string} Converted IP address
*/
getIP(number) {
let ip = [];
ip.push(number & 255);
ip.push((number >> 8) & 255);
ip.push((number >> 16) & 255);
ip.push((number >> 24) & 255);
ip.reverse();
return ip.join('.');
}
/**
* Sends a message to another adapter.
*
* @param {string} receiver
* @param {string} command
* @param {*} message Message to send to receiver, shall be an object and will be converted to such if another is given
* @param {function} (optional) Callback
* @param callback
* @returns void
*/
msg(receiver, command, message, callback) {
this._adapter.sendTo(
receiver,
command,
typeof message !== 'object' ? { message: message } : message,
callback === undefined ? function () {} : callback,
);
}
/**
* Capitalize first letter of a string
*
* @param {string} str String to capitalize
* @returns {string}
*/
ucFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Convert a date to timestamp.
*
* @param {Date} date Datetime to parse
* @returns {integer} parsed Timestamp
*/
getTimestamp(date) {
if (date === undefined || !date) {
return 0;
}
let ts = new Date(date).getTime();
return isNaN(ts) ? 0 : ts;
}
/**
* Convert a timestamp to datetime.
*
* @param {integer} ts Timestamp to be converted to date-time format (in ms)
* @returns {string} Timestamp in date-time format
*/
getDateTime(ts) {
if (ts === undefined || ts <= 0 || ts == '') {
return '';
}
let date = new Date(ts);
let day = `0${date.getDate()}`;
let month = `0${date.getMonth() + 1}`;
let year = date.getFullYear();
let hours = `0${date.getHours()}`;
let minutes = `0${date.getMinutes()}`;
let seconds = `0${date.getSeconds()}`;
return `${day.substr(-2)}.${month.substr(-2)}.${year} ${hours.substr(-2)}:${minutes.substr(
-2,
)}:${seconds.substr(-2)}`;
}
/**
* Get all instances of an adapter.
*
* @param {string} adapter Adapter to get instances of
* @param {function} callback Callback to invoke
* @returns void
*/
getAdapterInstances(adapter, callback) {
this._adapter.objects.getObjectView(
'system',
'instance',
{ startkey: `system.adapter.${adapter}.`, endkey: `system.adapter.${adapter}.\u9999` },
(err, instances) => {
if (instances && instances.rows) {
let result = [];
instances.rows.forEach(instance =>
result.push({
id: instance.id.replace('system.adapter.', ''),
config: instance.value.native.type,
}),
);
callback(null, result);
} else {
callback(`Could not retrieve ${adapter} instances!`);
}
},
);
}
/**
* Run Garbage Collector and delete outdated states / objects.
*
* @param {object} state Selected state
* @param {number} [ts] Objects older than this timespan will be deleted (in seconds)
* @param {boolean} [del] Whether to delete the object (or only empty its value)
* @param {number} [offset] Offset for the timespan in seconds
* @param whitelist
* @returns void
*/
runGarbageCollector(state, del = false, offset = 60, whitelist = []) {
this._adapter.log.debug(`Running Garbage Collector for ${state}...`);
this._adapter.getStates(`${state}.*`, (err, states) => {
if (err || !states) {
return;
}
// loop through states
let key;
for (let state in states) {
key = state.replace(`${this._adapter.name}.${this._adapter.instance}.`, '');
if (
this._STATES[key] &&
this._STATES[key].ts < Math.round(Date.now() / 1000) - offset &&
!(whitelist.length > 0 && RegExp(whitelist.join('|')).test(state))
) {
// apply deletion
this._adapter.log.debug(`Garbage Collector: ${del ? 'Deleted ' : 'Emptied '}${state}!`);
if (del) {
this._STATES[key] = undefined;
this._adapter.delObjectAsync(state);
} else {
this._setValue(key, '', { force: true });
}
}
}
});
}
/**
* Get a device state.
*
* @param state
* @param property
*/
getDeviceState(state, property = 'val') {
return this._STATES[state] !== undefined ? this._STATES[state][property] || false : null;
}
/**
* Set a device state.
*
* @param state
* @param value
*/
setDeviceState(state, value) {
if (
(this._STATES[state] === null || this._STATES[state] === undefined || this._STATES[state].val != value) &&
this._adapter &&
this._adapter.log &&
((this.options.updatesInLog && !this.options.updatesExceptions) ||
(this.options.updatesInLog &&
this.options.updatesExceptions &&
Array.isArray(this.options.updatesExceptions) &&
this.options.updatesExceptions.indexOf(state.substr(state.lastIndexOf('.') + 1)) == -1))
) {
this._adapter.log.debug(
`Updated state ${state} to value ${value} (from ${this._STATES[state] && this._STATES[state].val}).`,
);
}
return this.setDeviceProperties(state, {
val: value,
});
}
/**
* Set a device properties.
*
* @param state
* @param properties
*/
setDeviceProperties(state, properties) {
this._STATES[state] = {
...(this._STATES[state] || {}),
...(properties || {}),
ts: Math.round(Date.now() / 1000),
};
return true;
}
/**
* Deletes a state / object.
*
* @param {string} state State to be deleted
* @param {boolean} [nested] Whether to delete nested states as well
* @param {function} [callback] Callback to be invoked once finished deleting all states
* @returns void
*/
del(state, nested, callback) {
// create state to have at least one deletion (in case no states exist at all)
this._createNode({ node: state, description: 'DELETED' }, () => {
// get state tree
this._adapter.getStates(nested ? `${state}.*` : state, (err, objects) => {
if (err) {
this._adapter.log.warn(`Could not read states for deletion (${state}): ${err.message}`);
callback && callback();
return;
}
let deleted = 0;
objects = Object.keys(objects);
objects.push(state);
this._adapter.log.silly(`Found ${objects.length} objects in state ${state} to delete..`);
objects.forEach(object => {
this._adapter.delObject(object, err => {
if (err) {
this._adapter.log.warn(`Could not delete state object ${object}: ${err.message}`);
}
this._STATES[object.replace(`${this._adapter.namespace}.`, '')] = undefined;
deleted++;
if (deleted == objects.length) {
this._adapter.log.debug(`Deleted state ${state} with ${deleted} objects.`);
callback && callback();
}
});
});
});
});
}
/**
* Set multiple values and create the necessary nodes for it in case they are missing.
*
* @param {object} values
* @param {object} nodes
* @param {object} options
* @returns void
*/
setMultiple(nodes, values, options = {}) {
for (let key in values) {
if (nodes[key] && nodes[key].node && nodes[key].description) {
let node = nodes[key];
let value = values[key];
// replace options if given
options.placeholders = options.placeholders || {};
for (let placeholder in options.placeholders) {
node.node = node.node.replace(placeholder, options.placeholders[placeholder]);
node.description = node.description.replace(placeholder, options.placeholders[placeholder]);
}
// convert data if necessary
switch (node.convert) {
case 'string':
if (value && Array.isArray(value)) {
value = value.join(', ');
}
break;
case 'datetime':
this.set(
{
node: `${node.node}Datetime`,
description: node.description.replace('Timestamp', 'Date-Time'),
common: { type: 'string', role: 'text' },
},
value ? this.getDateTime(value * 1000) : '',
);
break;
}
// set node
this.set(node, value, options);
}
}
}
/**
* Set a value and create the necessary nodes for it in case it is missing.
*
* @param {object} node
* @param {string} node.node Node (= state) to set the value (and create in case it does not exist)
* @param {string} node.description Description of the node (in case it will be created)
* @param {object} node.common Common Details of the node (in case it will be created)
* @param {string} node.common.role Role of the node (in case it will be created)
* @param {string} node.common.type Type of the node (in case it will be created)
* @param {object} node.native Native Details of the node (in case it will be created)
* @param {string} value Value to set (in any case)
* @param {object} [options] Additional options
* @returns void
*/
set(node, value, options = {}) {
// set value
if (node && node.node && this._STATES[node.node] !== undefined) {
this._setValue(node.node, value, options);
if (options.subscribe === true && this.subscriptions.indexOf(node.node) === -1) {
this._adapter.subscribeStates(node.node);
this.subscriptions.push(node.node);
}
}
// create node
else {
// catch error
if (!node || !node.node || (node.name === undefined && node.description === undefined)) {
this._adapter.log.error(`Error: State not properly defined (${JSON.stringify(node)})!`);
return false;
}
this._createNode(node, () => this._setValue(node.node, value, options), options);
}
return true;
}
/**
* Creates an object (channel or state).
*
* @param {object} node
* @param {string} node.node Node (= state) to set the value (and create in case it does not exist)
* @param {string} node.description Description of the node (in case it will be created)
* @param {object} node.common Common Details of the node (in case it will be created)
* @param {string} node.common.role Role of the node (in case it will be created)
* @param {string} node.common.type Type of the node (in case it will be created)
* @param {object} node.native Native Details of the node (in case it will be created)
* @param {function} callback Callback function to be invoked
* @param {object} [options] Additional options
* @returns void
*/
_createNode(node, callback, options = {}) {
if (!this._adapter) {
return Promise.reject('Adapter not defined!');
}
// remap properties to common
let type =
node.role == 'device' || node.role == 'channel' ? (node.role == 'device' ? 'device' : 'channel') : 'state';
let common = {
name: node.name || node.description,
role: (node.common && node.common.role) || node.role || 'state',
type: (node.common && node.common.type) || node.type || 'string',
write: false,
...(node.common || {}),
};
// special roles
if (common.role.indexOf('button') > -1 || common.role.indexOf('indicator') > -1) {
if (!common.type) {
common = { ...common, type: 'boolean' };
}
common = { ...common, read: false, write: true };
} else if (common.role == 'device' || common.role == 'channel') {
common = { ...common, type: undefined, role: undefined };
}
// create object
this._adapter.setObjectNotExists(
node.node,
{
common: common,
type: type,
native: node.native || {},
},
(_err, _obj) => {
this._STATES[node.node] = null;
if (options.subscribe === true && this.subscriptions.indexOf(node.node) === -1) {
this._adapter.subscribeStates(node.node);
this.subscriptions.push(node.node);
}
callback && callback();
},
);
}
/**
* Sets a value of a state.
*
* @param {string} state State the value shall be set
* @param {string} value Value to be set
* @param {object} [options] Additional options
* @param {boolean} [options.force] Force to set value
* @returns void
*/
_setValue(state, value, options = {}) {
if (state !== undefined) {
if (
value !== undefined &&
(options.force ||
this._STATES[state] === undefined ||
this._STATES[state] === null ||
this._STATES[state].val != value)
) {
this.setDeviceState(state, value);
this._adapter.setStateAsync(state, { val: value, ts: Date.now(), ack: true });
} else {
this.setDeviceProperties(state);
}
}
}
/**
* Reset all states.
*
* @param void
* @returns void
*/
resetStates() {
this._STATES = {};
}
/**
* Read given data and set states.
*
* @param key
* @param data
* @param prefix
*/
readData(key, data, prefix) {
// only proceed if data is given
if (data === undefined || data === 'undefined') {
return false;
}
// get node details
key = key ? this.clean(key, false, '_') : '';
let node = this.getNode(prefix && prefix.indexOf('users.') > -1 ? `users.${key}` : key);
// add node details
if (key.indexOf('.state') > -1) {
node = Object.assign({}, node, {
common: { states: prefix.indexOf('opener') === -1 ? _LOCK.STATES : _OPENER.STATES },
});
}
if (key.indexOf('.lastAction') > -1) {
node = Object.assign({}, node, {
common: { states: prefix.indexOf('opener') === -1 ? _LOCK.ACTIONS : _OPENER.ACTIONS },
});
}
// loop nested data
if (data !== null && typeof data === 'object' && !node.convert && node.convert !== 'array') {
if (Object.keys(data).length > 0) {
// create channel
if (node.role === 'channel') {
this.set({
node: `${prefix}.${node.state || key}`,
role: 'channel',
description: node.description || this.ucFirst(key.substr(key.lastIndexOf('.') + 1)),
});
}
// read nested data
for (let nestedKey in data) {
this.readData((key ? `${key}.` : '') + nestedKey, data[nestedKey], prefix);
}
}
}
// write to states
else {
// convert data
data = this.convertNode(node, data, prefix);
// skip
if (node.skip) {
return;
}
// create channel if node.state is nested
if (
node.state &&
node.state.indexOf('.') > -1 &&
`${prefix}.${node.state.substr(0, node.state.lastIndexOf('.'))}` !==
`${prefix}.${key.substr(0, key.lastIndexOf('.'))}`
) {
this.readData(
node.state.substr(0, node.state.indexOf('.')),
{ [node.state.substr(node.state.indexOf('.') + 1)]: data },
prefix,
);
}
// state
let state = JSON.parse(JSON.stringify(node)); // copy node
state.node = `${prefix}.${node.state || key}`;
state.description = node.description || this.ucFirst(key.substr(key.lastIndexOf('.') + 1));
// config state
let config = state.node.toLowerCase().indexOf('config') > -1;
// set state
state.common = { ...(node.common || {}), write: config };
this.set(state, data, { subscribe: config });
}
}
/**
* Convert.
*
* @param node
* @param data
* @param prefix
* @param _prefix
*/
convertNode(node, data, _prefix) {
// flatten Array
if (Array.isArray(data)) {
data = data.join(',');
}
const dataType = typeof data;
if (
node &&
node.description === '(no description given)' &&
node.type === 'string' &&
!node.convert &&
dataType !== 'string'
) {
// it seems like a fallback object definition ... maybe we can fix the type
if (dataType === 'boolean' || dataType === 'number') {
node.type = dataType;
}
}
if (node.type === 'number' && dataType === 'string') {
if (data.includes('.')) {
data = parseFloat(data);
} else {
data = parseInt(data);
}
}
// type is boolean, but states are given
if (node.type === 'boolean' && node.common && node.common.states) {
data = node.common.states[data] === 'true';
}
// type is boolean, but number given
if (node.type === 'boolean' && Number.isInteger(data) && !(node.common && node.common.states)) {
data = data === 1;
}
return data;
}
}
module.exports = Library;