iobroker.nuki2
Version:
Nuki Smart Lock 2.0
511 lines (451 loc) • 13.5 kB
JavaScript
'use strict';
/**
* Library
*
* @description Library of general functions as well as helping functions handling ioBroker
* @author Zefau <https://github.com/Zefau/>
* @license MIT License
* @version 0.18.0
* @date 2019-08-18
*
*/
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
*
*/
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'];
this._STATES = {};
}
/**
* Terminate adapter.
*
* @param {string} [message=Terminating adapter due to error!] Message to display
* @param {boolean} [kill=false] Whether to kill the adapter (red lights) or not (yellow lights)
* @param {integer} [reason=11] Reason code for exit
* @return 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=false] if String shall be return in lower case
* @param {array} [characters=['<', '>', ' ', ',', ';', '!', '?']] Characters to be removed from string
* @param {string} [replacement=''] Characters shall be replaced with this character
* @return {string} Cleaned String
*
*/
clean(string, lowerCase, characters, replacement)
{
characters = characters ? characters : ['<', '>', ' ', ',', ';', '!', '?'];
characters.forEach(function(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
* @return void
*
*/
wait(time, callback)
{
setTimeout(() => callback, time);
}
/**
* Encode a string.
*
* @param {string} key Key used for encoding
* @param {string} string String to encode
* @return {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
* @return {string} Decoded String
*
*/
decode(key, string)
{
return this.encode(key, string);
}
/**
* Get a random key.
*
* @param {integer} length Length of key
* @return {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
* @return {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
* @return 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
* @return {string}
*
*/
ucFirst(str)
{
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Convert a date to timestamp.
*
* @param {date} date Datetime to parse
* @return {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)
* @return {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);
}
/**
* Run Duty Cycle and delete outdated states / objects.
*
* @param {string} states State tree to be deleted
* @param {number} kill in seconds
* @param {number} [variance=5] in seconds
* @return void
*
*/
runDutyCycle(states, kill, variance)
{
let that = this;
var tree = states.split('.');
if (tree.length > 0)
{
that._adapter.getStatesOf(tree[tree.length-2] || '', tree[tree.length-1], function(err, states)
{
states.forEach(function(state)
{
that._adapter.getState(state._id, function(err, info)
{
let ts = 0, lc = 0;
if (info === undefined || info === null || info.lc === undefined)
that._adapter.log.silly('Duty Cycle: ID ' + state._id + ' never updated!');
else
{
ts = (Math.floor(info.ts/1000) + (variance ? variance : 100));
lc = (Math.floor(info.lc/1000) + (variance ? variance : 100));
}
if (ts < kill && lc < kill)
{
that._adapter.log.silly('Duty Cycle: Deleted ' + state._id + ' (created ' + ts + ' & last change ' + lc + ' < ' + kill + ')!');
that._STATES[object] = undefined;
that._adapter.delObject(state._id);
}
});
});
});
}
}
/**
*
*
*
*/
getDeviceState(key)
{
return this._STATES[key] !== undefined ? this._STATES[key] : null;
}
/**
*
*
*
*/
setDeviceState(key, value)
{
this._STATES[key] = value;
}
/**
* Deletes a state / object.
*
* @param {string|array} states State(s) or state tree to be deleted
* @param {boolean} [nested=true] Whether to delete nested states as well
* @param {function} [callback] Callback to be invoked once finished deleting all states
* @return void
*
*/
del(states, nested, callback)
{
let that = this;
let finished = 0;
states = typeof states == 'string' ? [states] : states;
states.forEach(function(state)
{
// create state to have at least one deletion (in case no states exist at all)
that._createNode({ node: state }, function()
{
// get state tree
that._adapter.getStates(nested ? state + '.*' : state, function(err, objects)
{
let deleted = 0;
objects = Object.keys(objects);
// no states to delete, invoke callback
if (objects.length == 0)
callback && callback();
// delete child states
else
{
objects.forEach(function(object)
{
that._adapter.delObject(object, function(err)
{
that._STATES[object.replace(that._adapter.namespace + '.', '')] = undefined;
deleted++;
if (deleted == objects.length)
{
finished++;
if (finished == states.length && callback !== undefined)
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
* @return void
*
*/
setMultiple(values, nodes, options)
{
let that = this;
options = options !== undefined ? 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
for (let option in options)
{
node.node = node.node.replace(option, options[option]);
node.description = node.description.replace(option, options[option]);
}
// convert data if necessary
switch(node.convert)
{
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);
}
}
}
/**
* 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)
* @return {boolean}
*
*/
set(node, value)
{
let that = this;
// catch error
if (node.node === undefined || (node.name === undefined && node.description === undefined))
{
this._adapter.log.error('Error: State not properly defined (' + JSON.stringify(node) + ')!');
return false;
}
// create node
if (this._STATES[node.node] === undefined)
return this._createNode(node, () => that._setValue(node.node, value));
// set value
else
return this._setValue(node.node, value);
}
/**
* 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
* @return {boolean}
*
*/
_createNode(node, callback)
{
let that = this;
let common = {};
if (node.description !== undefined) common.name = node.description;
if (node.role !== undefined) common.role = node.role;
if (node.type !== undefined) common.type = node.type;
if (common.role && common.role.indexOf('button') > -1) { common.type = 'boolean'; common.read = false; common.write = true; }
this._adapter.setObjectNotExists(
node.node,
{common: Object.assign({name: node, role: 'state', type: 'string', 'write': false}, node.common || {}, common), type: 'state', native: node.native || {}},
function(err, obj)
{
if (obj !== undefined)
that._adapter.log.silly('Created node ' + JSON.stringify(obj));
that._STATES[node.node] = null;
callback && callback();
}
);
return true;
}
/**
* Sets a value of a state.
*
* @param {string} state State the value shall be set
* @param {string} value Value to be set
* @return void
*
*/
async _setValue(state, value)
{
if (value !== undefined && (this._STATES[state] != value || this._STATES[state] === undefined))
{
if ((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] + ').');
await this._adapter.setStateAsync(state, { val: value, ts: Date.now(), ack: true });
this.setDeviceState(state, value);
}
}
/**
* Reset all states.
*
* @param void
* @return void
*
*/
resetStates()
{
this._STATES = {};
}
}
module.exports = Library;