UNPKG

iobroker.nuki-extended

Version:
761 lines (683 loc) 25.5 kB
'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;