From 40eed3c825f4e26fd52483357784931ac58f789e Mon Sep 17 00:00:00 2001 From: foxriver76 Date: Thu, 28 Sep 2023 09:02:35 +0200 Subject: [PATCH] use eslint --- .eslintrc.json | 3 + lib/tools.js | 22 +- main.js | 1443 ++++++++++++++++++++++--------------------- package.json | 3 +- prettier.config.js | 1 + test/lib/setup.js | 289 +++++---- test/mocha.setup.js | 4 +- test/testStartup.js | 64 +- 8 files changed, 990 insertions(+), 839 deletions(-) create mode 100644 .eslintrc.json create mode 100644 prettier.config.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..4b9cdfb --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@foxriver76/eslint-config"] +} \ No newline at end of file diff --git a/lib/tools.js b/lib/tools.js index ec0ad32..39666bd 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -19,7 +19,9 @@ function isObject(it) { * @returns {it is any[]} */ function isArray(it) { - if (typeof Array.isArray === 'function') return Array.isArray(it); + if (typeof Array.isArray === 'function') { + return Array.isArray(it); + } return Object.prototype.toString.call(it) === '[object Array]'; } @@ -30,7 +32,7 @@ function isArray(it) { * @param {string} [yandexApiKey] The yandex API key. You can create one for free at https://translate.yandex.com/developers * @returns {Promise} */ -async function translateText(text, targetLang, yandexApiKey) { +function translateText(text, targetLang, yandexApiKey) { if (targetLang === 'en') { return text; } else if (!text) { @@ -55,8 +57,10 @@ async function translateYandex(text, targetLang, apiKey) { targetLang = 'zh'; } try { - const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`; - const response = await axios({url, timeout: 15000}); + const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=${encodeURIComponent( + text + )}&lang=en-${targetLang}`; + const response = await axios({ url, timeout: 15000 }); if (response.data && response.data.text && isArray(response.data.text)) { return response.data.text[0]; } @@ -74,8 +78,10 @@ async function translateYandex(text, targetLang, apiKey) { */ async function translateGoogle(text, targetLang) { try { - const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}&ie=UTF-8&oe=UTF-8`; - const response = await axios({url, timeout: 15000}); + const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent( + text + )}&ie=UTF-8&oe=UTF-8`; + const response = await axios({ url, timeout: 15000 }); if (isArray(response.data)) { // we got a valid response return response.data[0][0][0]; @@ -83,9 +89,7 @@ async function translateGoogle(text, targetLang) { throw new Error('Invalid response for translate request'); } catch (e) { if (e.response && e.response.status === 429) { - throw new Error( - `Could not translate to "${targetLang}": Rate-limited by Google Translate` - ); + throw new Error(`Could not translate to "${targetLang}": Rate-limited by Google Translate`); } else { throw new Error(`Could not translate to "${targetLang}": ${e}`); } diff --git a/main.js b/main.js index 7603cc0..83a7ca1 100755 --- a/main.js +++ b/main.js @@ -3,708 +3,759 @@ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); -const mqtt = require('mqtt'); - -let _context = { - custom: {}, //cache object's mqtt-client settings - subTopics: {}, //subscribed mqtt topics - topic2id: {}, //maps mqtt topics to ioBroker ids - addTopics: {}, //additional mqtt topics to subscribe to - addedTopics: {}, //received mqtt topics that created a new object (addTopics) +const mqtt = require('mqtt'); + +const _context = { + custom: {}, //cache object's mqtt-client settings + subTopics: {}, //subscribed mqtt topics + topic2id: {}, //maps mqtt topics to ioBroker ids + addTopics: {}, //additional mqtt topics to subscribe to + addedTopics: {} //received mqtt topics that created a new object (addTopics) }; - class MqttClient extends utils.Adapter { - - /** - * @param {Partial} [options={}] - */ - - constructor(options) { - super({ - ...options, - name: 'mqtt-client', - }); - this.on('ready', this.onReady.bind(this)); - this.on('objectChange', this.onObjectChange.bind(this)); - this.on('stateChange', this.onStateChange.bind(this)); - this.on('message', this.onMessage.bind(this)); - this.on('unload', this.onUnload.bind(this)); - this._connected = false; - this.client = null; - this._subscribes = []; - this.adapterFinished = false; - } - - connect() { - this.log.info('connected to broker'); - - if (!this._connected) { - this._connected = true; - this.setState('info.connection', true, true); - } - - if (this.config.onConnectTopic && this.config.onConnectMessage) { - let topic = this.config.onConnectTopic; - - this.client.publish(this.topicAddPrefixOut(topic), this.config.onConnectMessage, { qos: 2, retain: true }, () => - this.log.debug('successfully published ' + JSON.stringify({ topic: topic, message: this.config.onConnectMessage }))); - } - - const subTopics = _context.subTopics; - const addTopics = _context.addTopics; - - //initially subscribe to topics - if (Object.keys(subTopics).length) { - this.subscribe(subTopics, () => - this.log.debug('subscribed to: ' + JSON.stringify(subTopics))); - } - - if (Object.keys(addTopics).length) { - this.subscribe(addTopics, () => - this.log.debug('subscribed to additional topics: ' + JSON.stringify(addTopics))); - } - } - - reconnect() { - this.log.debug('trying to reconnect to broker'); - } - - disconnect() { - if (this._connected) { - this._connected = false; - this.setState('info.connection', false, true); - } - this.log.warn('disconnected from broker'); - } - - offline() { - if (this._connected) { - this._connected = false; - this.setState('info.connection', false, true); - } - this.log.warn('client offline'); - } - - error(err) { - this.log.warn('client error: ' + err); - } - - message(topic, msg) { - const custom = _context.custom; - const topic2id = _context.topic2id; - const addedTopics = _context.addedTopics; - msg = msg.toString(); - - topic = this.topicRemovePrefixIn(topic); - - //if topic2id[topic] does not exist automatically convert topic to id with guiding adapter namespace - const id = topic2id[topic] || this.convertTopic2ID(topic, this.namespace); - - this.log.debug(`received message ${msg} for id ${id}=>${JSON.stringify(custom[id])}`); - - if (topic2id[topic] && custom[id] && custom[id].subscribe) { - if (custom[id].subAsObject) { - this.setStateObj(id, msg); - } else { - this.setStateVal(id, msg); - } - } else if (!addedTopics[topic]) { - //prevents object from being recreated while first creation has not finished - addedTopics[topic] = null; - let obj = { - type: 'state', - role: 'text', - common: { - name: id.split('.').pop(), - type: 'mixed', - read: true, - write: true, - desc: 'created from topic', - custom: { - } - }, - native: { - topic: topic - } - }; - obj.common.custom[this.namespace] = { - enabled: true, - topic: topic, - publish: false, - pubChangesOnly: false, - pubAsObject: false, - qos: 0, - retain: false, - subscribe: true, - subChangesOnly: false, - subAsObject: false, - subQos: 0, - setAck: true - }; - this.setObjectNotExists(id, obj, () => - this.log.debug('created and subscribed to new state: ' + id)); - //onObjectChange should now receive this object - } else { - this.log.debug('state already exists'); - } - } - - setStateObj(id, msg) { - this.getForeignState(id, (err, state) => { - try { - const obj = JSON.parse(msg); - this.log.debug(JSON.stringify(obj)); - - if (obj.hasOwnProperty('val')) { - const custom = _context.custom; - if (obj.hasOwnProperty('ts') && state && obj.ts <= state.ts) { - this.log.debug('object ts not newer than current state ts: ' + msg); - return false; - } - if (obj.hasOwnProperty('lc') && state && obj.lc < state.lc) { - this.log.debug('object lc not newer than current state lc: ' + msg); - return false; - } - // todo: !== correct??? - if (this.config.inbox === this.config.outbox && - custom[id].publish && - !obj.hasOwnProperty('ts') && - !obj.hasOwnProperty('lc') && - obj.val !== state.val) { - this.log.debug('object value did not change (loop protection): ' + msg); - return false; - } - // todo: !== correct??? - if (custom[id].subChangesOnly && obj.val !== state.val) { - this.log.debug('object value did not change: ' + msg); - return false; - } - if (custom[id].setAck) obj.ack = true; - delete obj.from; - this.setForeignState(id, obj); - this.log.debug('object set (as object) to ' + JSON.stringify(obj)); - return true; - } else { - this.log.warn('no value in object: ' + msg); - return false; - } - } catch (e) { - this.log.warn('could not parse message as object: ' + msg); - return false; - } - }); - } - - setStateVal(id, msg) { - const custom = _context.custom; - //this.log.debug('state for id: '+ id); - this.getForeignState(id, (err, state) => { - - if (state && this.val2String(state.val) === msg) { - //this.log.debug('setVAL: ' + JSON.stringify(state) + '; value: ' + this.val2String(state.val) + '=> ' + msg); - if (this.config.inbox === this.config.outbox && custom[id] && custom[id].publish) { - this.log.debug('value did not change (loop protection)'); - return false; - } else if (custom[id] && custom[id].subChangesOnly) { - this.log.debug('value did not change'); - return false; - } - } - const _state = {val: this.stringToVal(custom, id, msg), ack: custom[id] && custom[id].setAck}; - this.setForeignState(id, _state); - this.log.debug('value of ' + id + ' set to ' + JSON.stringify(_state)); - return true; - }); - } - - publishState(id, state) { - if (this.client) { - const custom = _context.custom; - const settings = custom[id]; - if (!settings || !state) return false; - if (custom[id].pubState && settings.pubChangesOnly && (state.ts !== state.lc)) return false; - - custom[id].pubState = state; - this.log.debug('publishing ' + id); - - let topic = settings.topic; - - const message = settings.pubAsObject ? JSON.stringify(state) : this.val2String(state.val); - - this.client.publish(this.topicAddPrefixOut(topic), message, { qos: settings.qos, retain: settings.retain }, () => - this.log.debug(`successfully published ${id}: ${JSON.stringify({topic: topic, message: message})}`)); - - return true; - } - } - - topicAddPrefixOut(topic) { - //add outgoing prefix - return topic = this.config.outbox ? this.config.outbox + '/' + topic : topic; - } - - topicAddPrefixIn(topic) { - //add outgoing prefix - return topic = this.config.inbox ? this.config.inbox + '/' + topic : topic; - } - - topicRemovePrefixIn(topic) { - if (this.config.inbox && topic.substring(0, this.config.inbox.length) === this.config.inbox) { - topic = topic.substr(this.config.inbox.length + 1); - } - return topic; - } - - unpublish(id) { - if (this.client) { - const custom = _context.custom; - const settings = custom[id]; - if (!settings) return false; - - custom[id].pubState = null; - this.log.debug('unpublishing ' + id); - - let topic = settings.topic; - - this.client.publish(this.topicAddPrefixOut(topic), null, { qos: settings.qos, retain: false }, () => - this.log.debug('successfully unpublished ' + id)); - - return true; - } - } - - subscribe(topics, callback) { - if (this.client) { - let subTopics = {}; - - for (const key of Object.keys(topics)) { - subTopics[this.topicAddPrefixIn(key)] = {qos: topics[key]}; - } - - //this.log.debug('Subscribed: ' + subTopics); - this.log.debug(`trying to subscribe to ${Object.keys(subTopics).length} topics: ` + JSON.stringify(subTopics)); - this.client.subscribe(subTopics, (err, granted) => { - if (!err) - this.log.debug(`successfully subscribed to ${granted.length} topics`); - else - this.log.debug(`error subscribing to ${Object.keys(subTopics).length} topics`) - callback(); - }); - } - } - - unsubscribe(topic, callback) { - this.client && this.client.unsubscribe(this.topicAddPrefixIn(topic), callback); - } - - iobSubscribe(id, callback) { - if (!this._subscribes.includes(id)) { - this._subscribes.push(id); - this._subscribes.sort(); - this.subscribeForeignStates(id, callback); - } - } - - iobUnsubscribe(id) { - const pos = this._subscribes.indexOf(id); - if (pos !== -1) { - this._subscribes.splice(pos, 1); - this.unsubscribeForeignStates(id); - } - } - - val2String(val) { - return val === null ? 'null' : (val === undefined ? 'undefined' : val.toString()); - } - - stringToVal(custom, id, val) { - if (val === 'undefined') { - return undefined; - } - if (val === 'null') { - return null; - } - if (!custom[id] || !custom[id].type || custom[id].type === 'string' || custom[id].type === 'mixed') { - return val; - } - - if (custom[id].type === 'number') { - if (val === true || val === 'true') val = 1; - if (val === false || val === 'false') val = 0; - if (typeof val.toString === 'function') { - val = val.toString().replace(',', '.'); - } - val = parseFloat(val) || 0; - return val; - } - if (custom[id].type === 'boolean') { - if (val === '1' || val === 'true') val = true; - if (val === '0' || val === 'false') val = false; - return !!val; - } - return val; - } - - convertID2Topic(id, namespace) { - let topic; - - //if necessary remove namespace before converting, e.g. "mqtt-client.0..." - if (namespace && id.substring(0, namespace.length) === namespace) { - topic = id.substring(namespace.length + 1); - } else { - topic = id; - } - - //replace dots with slashes and underscores with spaces - topic = topic.replace(/\./g, '/').replace(/_/g, ' '); - return topic; - } - - convertTopic2ID(topic, namespace) { - if (!topic) { - return topic; - } - - //replace slashes with dots and spaces with underscores - topic = topic.replace(/\//g, '.').replace(/\s/g, '_'); - - //replace guiding and trailing dot - if (topic[0] === '.') topic = topic.substring(1); - if (topic[topic.length - 1] === '.') topic = topic.substring(0, topic.length - 1); - - //add namespace to id if exists - //if (namespace && namespace !== '') { - // topic = namespace + '.' + topic; - //} - return topic; - } - - checkSettings(id, custom, aNamespace, qos, subQos) { - custom.topic = custom.topic || this.convertID2Topic(id, aNamespace); - custom.enabled = custom.enabled === true; - custom.publish = custom.publish === true; - custom.pubChangesOnly = custom.pubChangesOnly === true; - custom.pubAsObject = custom.pubAsObject === true; - custom.retain = custom.retain === true; - custom.qos = parseInt(custom.qos || qos, 10) || 0; - - custom.subscribe = custom.subscribe === true; - custom.subChangesOnly = custom.subChangesOnly === true; - custom.subAsObject = custom.subAsObject === true; - custom.setAck = custom.setAck !== false; - custom.subQos = parseInt(custom.subQos || subQos, 10) || 0; - } - - getObjects(adapter, ids, callback, _result) { - _result = _result || {}; - //const that=this; - if (!ids || !ids.length) { - callback(_result); - } else { - //adapter.log.info('IDs:' + ids); - adapter.getForeignObject(ids.shift(), (err, obj) => { - if (obj) { - _result[obj._id] = obj; - } - setImmediate(adapter.getObjects, adapter, ids, callback, _result); - }); - } - } - - main() { - this.getState('info.connection', (err, state) => { - (!state || state.val) && this.setState('info.connection', false, true); - - this.config.inbox = this.config.inbox.trim(); - this.config.outbox = this.config.outbox.trim(); - - if (this.config.host && this.config.host !== '') { - const custom = _context.custom; - const subTopics = _context.subTopics; - const topic2id = _context.topic2id; - const addTopics = _context.addTopics; - - const protocol = '' + (this.config.websocket ? 'ws' : 'mqtt') + (this.config.ssl ? 's' : ''); - const _url = `${protocol}://${this.config.username ? (this.config.username + ':' + this.config.password + '@') : ''}${this.config.host}${this.config.port ? (':' + this.config.port) : ''}?clientId=${this.config.clientId}`; - const __url = `${protocol}://${this.config.username ? (this.config.username + ':*******************@') : ''}${this.config.host}${this.config.port ? (':' + this.config.port) : ''}?clientId=${this.config.clientId}`; - - this.getObjectView('system', 'custom', {}, (err, doc) => { - const ids = []; - if (doc && doc.rows) { - for (let i = 0, l = doc.rows.length; i < l; i++) { - const cust = doc.rows[i].value; - if (cust && cust[this.namespace] && cust[this.namespace].enabled) { - ids.push(doc.rows[i].id); - } - } - } - - // we need type of object - this.getObjects(this, ids, objs => { - for (const id of Object.keys(objs)) { - custom[id] = objs[id].common.custom[this.namespace]; - custom[id].type = objs[id].common.type; - - this.checkSettings(id, custom[id], this.namespace, this.config.qos, this.config.subQos); - - if (custom[id].subscribe) { - subTopics[custom[id].topic] = custom[id].subQos; - topic2id[custom[id].topic] = id; - } - - // subscribe on changes - if (custom[id].enabled) { - this.iobSubscribe(id); - } - - this.log.debug('enabled syncing of ' + id + ' (publish/subscribe:' + custom[id].publish.toString() + '/' + custom[id].subscribe.toString() + ')'); - } - this.log.debug('complete Custom: ' + JSON.stringify(custom)); - - if (this.config.subscriptions) { - for (const topic of this.config.subscriptions.split(',')) { - if (topic && topic.trim()) { - addTopics[topic.trim()] = 0; // QoS - } - } - } - this.log.debug(`found ${Object.keys(addTopics).length} additional topic to subscribe to`); - - let will = undefined; - - if (this.config.lastWillTopic && this.config.lastWillMessage) { - this.log.info(`Try to connect to ${__url}, protocol version ${this.config.mqttVersion} with lwt "${this.config.lastWillTopic}"`); - - will = { - topic: this.topicAddPrefixOut(this.config.lastWillTopic), - payload: this.config.lastWillMessage, - qos: 2, - retain: true - }; - } else { - this.log.info('Try to connect to ' + __url); - } - const mqttVersion = Number.parseInt(this.config.mqttVersion || 4); - try { - this.client = mqtt.connect(_url, { - host: this.config.host, - port: this.config.port, - protocolVersion: mqttVersion, - ssl: this.config.ssl, - rejectUnauthorized: this.config.rejectUnauthorized, - reconnectPeriod: this.config.reconnectPeriod, - username: this.config.username, - password: this.config.password, - clientId: this.config.clientId, - clean: true, - will - }); - } catch (e) { - this.log.error(e); - this.finish(() => { - setTimeout(() => this.terminate ? this.terminate() : process.exit(0), 200); - }); - return; - } - - this.client.on('connect', this.connect.bind(this)); - this.client.on('reconnect', this.reconnect.bind(this)); - this.client.on('disconnect', this.disconnect.bind(this)); - this.client.on('offline', this.offline.bind(this)); - this.client.on('message', this.message.bind(this)); - this.client.on('error', this.error.bind(this)); - }); - }); - } - - this.subscribeForeignObjects('*'); - }); - } - - /** - * Is called when databases are this. and adapter received configuration. - */ - async onReady() { - this.main(); - } - - /** - * Is called when adapter shuts down - callback has to be called under any circumstances! - * @param {() => void} callback - */ - finish(callback) { - if (this.adapterFinished) { - return; - } - if (this.client && this.config.onDisconnectTopic && this.config.onDisconnectMessage) { - let topic = this.config.onDisconnectTopic; - - this.log.info(`Disconnecting with message "${this.config.onDisconnectMessage}" on topic "${topic}"`); - this.client.publish(this.topicAddPrefixOut(topic), this.config.onDisconnectMessage, { qos: 2, retain: true }, () => { - this.log.debug('successfully published ' + JSON.stringify({ topic: topic, message: this.config.onDisconnectMessage })); - this.end(callback); - }); - } else { - this.end(callback); - } - } - - /** - * Is called when adapter shuts down - callback has to be called under any circumstances! - * @param {() => void} callback - */ - end(callback) { - this.adapterFinished = true; - this.client && this.client.end(() => { - this.log.debug(`closed client`); - this.setState('info.connection', false, true); - callback && callback(); - }); - } - - /** - * Is called when adapter shuts down - callback has to be called under any circumstances! - * @param {() => void} callback - */ - onUnload(callback) { - try { - this.finish(callback); - //if (callback) callback(); - } catch (e) { - if (callback) callback(); - } - } - - - /** - * Is called if a subscribed object changes - * @param {string} id - * @param {ioBroker.Object | null | undefined} obj - */ - onObjectChange(id, obj) { - const custom = _context.custom; - const subTopics = _context.subTopics; - const topic2id = _context.topic2id; - - if (obj && obj.common && obj.common.custom && obj.common.custom[this.namespace] && obj.common.custom[this.namespace].enabled) { - //const pubState = custom[id] ? custom[id].pubState : null; - //const state = custom[id] ? custom[id].state : null; - - custom[id] = obj.common.custom[this.namespace]; - //this.log.info('object common: ' + JSON.stringify(obj.common.custom[this.namespace])); - //custom[id].pubState = pubState; - //custom[id].state = state; - custom[id].type = obj.common.type; - - - this.checkSettings(id, custom[id], this.namespace, this.config.qos, this.config.subQos); - - if (custom[id].subscribe) { - subTopics[custom[id].topic] = custom[id].subQos; - topic2id[custom[id].topic] = id; - const sub = {}; - sub[custom[id].topic] = custom[id].subQos; - - this.subscribe(sub, () => { - this.log.debug('subscribed to ' + JSON.stringify(sub)); - }); - } else { - delete subTopics[custom[id].topic]; - delete topic2id[custom[id].topic]; - this.iobUnsubscribe(id); - - this.unsubscribe(custom[id].topic, () => - custom[id] && this.log.debug('unsubscribed from ' + custom[id].topic)); - } - - if (custom[id].enabled) { //@todo should this be .subscribe? - //subscribe to state changes - this.iobSubscribe(id, (err) => { - //publish state once - if (err || !custom[id].publish) - return; - this.getForeignState(id, (err, state) => { - if (err || !state) - return; - this.log.debug(`publish ${id} once: ${JSON.stringify(state)}`); - this.onStateChange(id, state); - }); - }); - } - - this.log.debug(`enabled syncing of ${id} (publish/subscribe:${custom[id].publish.toString()}/${custom[id].subscribe.toString()})`); - } else if (custom[id]) { - const topic = custom[id].topic; - - this.unsubscribe(topic, () => - this.log.debug('unsubscribed from ' + topic)); - - delete subTopics[custom[id].topic]; - delete topic2id[custom[id].topic] - - if (custom[id].publish) { - this.iobUnsubscribe(id); - } - - delete custom[id]; - - this.log.debug('disabled syncing of ' + id); - } - } - - /** - * Is called if a subscribed state changes - * @param {string} id - * @param {ioBroker.State | null | undefined} state - */ - onStateChange(id, state) { - const custom = _context.custom; - - if (custom[id]) { - custom[id].state = state; - - if (custom[id].enabled && custom[id].publish) { - if (!state) { - // The state was deleted/expired, make sure it is no longer retained - this.unpublish(id); - } else if (state.from !== 'system.adapter.' + this.namespace) { // prevent republishing to same broker - this.publishState(id, state); - } - } - } - } - - /** - * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... - * Using this method requires "common.message" property to be set to true in io-package.json - * @param {ioBroker.Message} obj - */ - onMessage(obj) { - if (typeof obj === 'object' && obj.command) { - if (obj.command === 'stopInstance') { - // e.g. send email or pushover or whatever - this.log.info('Stop Instance command received...'); - - this.finish(() => { - this.sendTo(obj.from, obj.command, 'Message received', obj.callback); - setTimeout(() => this.terminate ? this.terminate() : process.exit(0), 200); - }); - // Send response in callback if required - // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback); - } - } - } + /** + * @param {Partial} [options={}] + */ + + constructor(options) { + super({ + ...options, + name: 'mqtt-client' + }); + this.on('ready', this.onReady.bind(this)); + this.on('objectChange', this.onObjectChange.bind(this)); + this.on('stateChange', this.onStateChange.bind(this)); + this.on('message', this.onMessage.bind(this)); + this.on('unload', this.onUnload.bind(this)); + this._connected = false; + this.client = null; + this._subscribes = []; + this.adapterFinished = false; + } + + connect() { + this.log.info('connected to broker'); + + if (!this._connected) { + this._connected = true; + this.setState('info.connection', true, true); + } + + if (this.config.onConnectTopic && this.config.onConnectMessage) { + const topic = this.config.onConnectTopic; + + this.client.publish( + this.topicAddPrefixOut(topic), + this.config.onConnectMessage, + { qos: 2, retain: true }, + () => + this.log.debug( + 'successfully published ' + + JSON.stringify({ topic: topic, message: this.config.onConnectMessage }) + ) + ); + } + + const subTopics = _context.subTopics; + const addTopics = _context.addTopics; + + //initially subscribe to topics + if (Object.keys(subTopics).length) { + this.subscribe(subTopics, () => this.log.debug('subscribed to: ' + JSON.stringify(subTopics))); + } + + if (Object.keys(addTopics).length) { + this.subscribe(addTopics, () => + this.log.debug('subscribed to additional topics: ' + JSON.stringify(addTopics)) + ); + } + } + + reconnect() { + this.log.debug('trying to reconnect to broker'); + } + + disconnect() { + if (this._connected) { + this._connected = false; + this.setState('info.connection', false, true); + } + this.log.warn('disconnected from broker'); + } + + offline() { + if (this._connected) { + this._connected = false; + this.setState('info.connection', false, true); + } + this.log.warn('client offline'); + } + + error(err) { + this.log.warn('client error: ' + err); + } + + message(topic, msg) { + const custom = _context.custom; + const topic2id = _context.topic2id; + const addedTopics = _context.addedTopics; + msg = msg.toString(); + + topic = this.topicRemovePrefixIn(topic); + + //if topic2id[topic] does not exist automatically convert topic to id with guiding adapter namespace + const id = topic2id[topic] || this.convertTopic2ID(topic); + + this.log.debug(`received message ${msg} for id ${id}=>${JSON.stringify(custom[id])}`); + + if (topic2id[topic] && custom[id] && custom[id].subscribe) { + if (custom[id].subAsObject) { + this.setStateObj(id, msg); + } else { + this.setStateVal(id, msg); + } + } else if (!addedTopics[topic]) { + //prevents object from being recreated while first creation has not finished + addedTopics[topic] = null; + const obj = { + type: 'state', + role: 'text', + common: { + name: id.split('.').pop(), + type: 'mixed', + read: true, + write: true, + desc: 'created from topic', + custom: {} + }, + native: { + topic: topic + } + }; + obj.common.custom[this.namespace] = { + enabled: true, + topic: topic, + publish: false, + pubChangesOnly: false, + pubAsObject: false, + qos: 0, + retain: false, + subscribe: true, + subChangesOnly: false, + subAsObject: false, + subQos: 0, + setAck: true + }; + this.setObjectNotExists(id, obj, () => this.log.debug('created and subscribed to new state: ' + id)); + //onObjectChange should now receive this object + } else { + this.log.debug('state already exists'); + } + } + + setStateObj(id, msg) { + this.getForeignState(id, (err, state) => { + try { + const obj = JSON.parse(msg); + this.log.debug(JSON.stringify(obj)); + + if (obj.hasOwnProperty('val')) { + const custom = _context.custom; + if (obj.hasOwnProperty('ts') && state && obj.ts <= state.ts) { + this.log.debug('object ts not newer than current state ts: ' + msg); + return false; + } + if (obj.hasOwnProperty('lc') && state && obj.lc < state.lc) { + this.log.debug('object lc not newer than current state lc: ' + msg); + return false; + } + // todo: !== correct??? + if ( + this.config.inbox === this.config.outbox && + custom[id].publish && + !obj.hasOwnProperty('ts') && + !obj.hasOwnProperty('lc') && + obj.val !== state.val + ) { + this.log.debug('object value did not change (loop protection): ' + msg); + return false; + } + // todo: !== correct??? + if (custom[id].subChangesOnly && obj.val !== state.val) { + this.log.debug('object value did not change: ' + msg); + return false; + } + if (custom[id].setAck) { + obj.ack = true; + } + delete obj.from; + this.setForeignState(id, obj); + this.log.debug('object set (as object) to ' + JSON.stringify(obj)); + return true; + } else { + this.log.warn('no value in object: ' + msg); + return false; + } + } catch { + this.log.warn('could not parse message as object: ' + msg); + return false; + } + }); + } + + setStateVal(id, msg) { + const custom = _context.custom; + //this.log.debug('state for id: '+ id); + this.getForeignState(id, (err, state) => { + if (state && this.val2String(state.val) === msg) { + //this.log.debug('setVAL: ' + JSON.stringify(state) + '; value: ' + this.val2String(state.val) + '=> ' + msg); + if (this.config.inbox === this.config.outbox && custom[id] && custom[id].publish) { + this.log.debug('value did not change (loop protection)'); + return false; + } else if (custom[id] && custom[id].subChangesOnly) { + this.log.debug('value did not change'); + return false; + } + } + const _state = { val: this.stringToVal(custom, id, msg), ack: custom[id] && custom[id].setAck }; + this.setForeignState(id, _state); + this.log.debug('value of ' + id + ' set to ' + JSON.stringify(_state)); + return true; + }); + } + + publishState(id, state) { + if (this.client) { + const custom = _context.custom; + const settings = custom[id]; + if (!settings || !state) { + return false; + } + if (custom[id].pubState && settings.pubChangesOnly && state.ts !== state.lc) { + return false; + } + + custom[id].pubState = state; + this.log.debug('publishing ' + id); + + const topic = settings.topic; + + const message = settings.pubAsObject ? JSON.stringify(state) : this.val2String(state.val); + + this.client.publish( + this.topicAddPrefixOut(topic), + message, + { qos: settings.qos, retain: settings.retain }, + () => + this.log.debug( + `successfully published ${id}: ${JSON.stringify({ topic: topic, message: message })}` + ) + ); + + return true; + } + } + + topicAddPrefixOut(topic) { + //add outgoing prefix + return (topic = this.config.outbox ? this.config.outbox + '/' + topic : topic); + } + + topicAddPrefixIn(topic) { + //add outgoing prefix + return (topic = this.config.inbox ? this.config.inbox + '/' + topic : topic); + } + + topicRemovePrefixIn(topic) { + if (this.config.inbox && topic.substring(0, this.config.inbox.length) === this.config.inbox) { + topic = topic.substr(this.config.inbox.length + 1); + } + return topic; + } + + unpublish(id) { + if (this.client) { + const custom = _context.custom; + const settings = custom[id]; + if (!settings) { + return false; + } + + custom[id].pubState = null; + this.log.debug('unpublishing ' + id); + + const topic = settings.topic; + + this.client.publish(this.topicAddPrefixOut(topic), null, { qos: settings.qos, retain: false }, () => + this.log.debug('successfully unpublished ' + id) + ); + + return true; + } + } + + subscribe(topics, callback) { + if (this.client) { + const subTopics = {}; + + for (const key of Object.keys(topics)) { + subTopics[this.topicAddPrefixIn(key)] = { qos: topics[key] }; + } + + this.log.debug( + `trying to subscribe to ${Object.keys(subTopics).length} topics: ${JSON.stringify(subTopics)}` + ); + this.client.subscribe(subTopics, (err, granted) => { + if (!err) { + this.log.debug(`successfully subscribed to ${granted.length} topics`); + } else { + this.log.debug(`error subscribing to ${Object.keys(subTopics).length} topics`); + } + callback(); + }); + } + } + + unsubscribe(topic, callback) { + this.client && this.client.unsubscribe(this.topicAddPrefixIn(topic), callback); + } + + iobSubscribe(id, callback) { + if (!this._subscribes.includes(id)) { + this._subscribes.push(id); + this._subscribes.sort(); + this.subscribeForeignStates(id, callback); + } + } + + iobUnsubscribe(id) { + const pos = this._subscribes.indexOf(id); + if (pos !== -1) { + this._subscribes.splice(pos, 1); + this.unsubscribeForeignStates(id); + } + } + + val2String(val) { + return val === null ? 'null' : val === undefined ? 'undefined' : val.toString(); + } + + stringToVal(custom, id, val) { + if (val === 'undefined') { + return undefined; + } + if (val === 'null') { + return null; + } + if (!custom[id] || !custom[id].type || custom[id].type === 'string' || custom[id].type === 'mixed') { + return val; + } + + if (custom[id].type === 'number') { + if (val === true || val === 'true') { + val = 1; + } + if (val === false || val === 'false') { + val = 0; + } + if (typeof val.toString === 'function') { + val = val.toString().replace(',', '.'); + } + val = parseFloat(val) || 0; + return val; + } + if (custom[id].type === 'boolean') { + if (val === '1' || val === 'true') { + val = true; + } + if (val === '0' || val === 'false') { + val = false; + } + return !!val; + } + return val; + } + + convertID2Topic(id, namespace) { + let topic; + + //if necessary remove namespace before converting, e.g. "mqtt-client.0..." + if (namespace && id.substring(0, namespace.length) === namespace) { + topic = id.substring(namespace.length + 1); + } else { + topic = id; + } + + //replace dots with slashes and underscores with spaces + topic = topic.replace(/\./g, '/').replace(/_/g, ' '); + return topic; + } + + convertTopic2ID(topic) { + if (!topic) { + return topic; + } + + //replace slashes with dots and spaces with underscores + topic = topic.replace(/\//g, '.').replace(/\s/g, '_'); + + //replace guiding and trailing dot + if (topic[0] === '.') { + topic = topic.substring(1); + } + if (topic[topic.length - 1] === '.') { + topic = topic.substring(0, topic.length - 1); + } + + return topic; + } + + checkSettings(id, custom, aNamespace, qos, subQos) { + custom.topic = custom.topic || this.convertID2Topic(id, aNamespace); + custom.enabled = custom.enabled === true; + custom.publish = custom.publish === true; + custom.pubChangesOnly = custom.pubChangesOnly === true; + custom.pubAsObject = custom.pubAsObject === true; + custom.retain = custom.retain === true; + custom.qos = parseInt(custom.qos || qos, 10) || 0; + + custom.subscribe = custom.subscribe === true; + custom.subChangesOnly = custom.subChangesOnly === true; + custom.subAsObject = custom.subAsObject === true; + custom.setAck = custom.setAck !== false; + custom.subQos = parseInt(custom.subQos || subQos, 10) || 0; + } + + getObjects(adapter, ids, callback, _result) { + _result = _result || {}; + + if (!ids || !ids.length) { + callback(_result); + } else { + adapter.getForeignObject(ids.shift(), (err, obj) => { + if (obj) { + _result[obj._id] = obj; + } + setImmediate(adapter.getObjects, adapter, ids, callback, _result); + }); + } + } + + main() { + this.getState('info.connection', (err, state) => { + (!state || state.val) && this.setState('info.connection', false, true); + + this.config.inbox = this.config.inbox.trim(); + this.config.outbox = this.config.outbox.trim(); + + if (this.config.host && this.config.host !== '') { + const custom = _context.custom; + const subTopics = _context.subTopics; + const topic2id = _context.topic2id; + const addTopics = _context.addTopics; + + const protocol = '' + (this.config.websocket ? 'ws' : 'mqtt') + (this.config.ssl ? 's' : ''); + const _url = `${protocol}://${ + this.config.username ? this.config.username + ':' + this.config.password + '@' : '' + }${this.config.host}${this.config.port ? ':' + this.config.port : ''}?clientId=${this.config.clientId}`; + const __url = `${protocol}://${ + this.config.username ? this.config.username + ':*******************@' : '' + }${this.config.host}${this.config.port ? ':' + this.config.port : ''}?clientId=${this.config.clientId}`; + + this.getObjectView('system', 'custom', {}, (err, doc) => { + const ids = []; + if (doc && doc.rows) { + for (let i = 0, l = doc.rows.length; i < l; i++) { + const cust = doc.rows[i].value; + if (cust && cust[this.namespace] && cust[this.namespace].enabled) { + ids.push(doc.rows[i].id); + } + } + } + + // we need type of object + this.getObjects(this, ids, objs => { + for (const id of Object.keys(objs)) { + custom[id] = objs[id].common.custom[this.namespace]; + custom[id].type = objs[id].common.type; + + this.checkSettings(id, custom[id], this.namespace, this.config.qos, this.config.subQos); + + if (custom[id].subscribe) { + subTopics[custom[id].topic] = custom[id].subQos; + topic2id[custom[id].topic] = id; + } + + // subscribe on changes + if (custom[id].enabled) { + this.iobSubscribe(id); + } + + this.log.debug( + `enabled syncing of ${id} (publish/subscribe:${custom[id].publish.toString()}/${custom[ + id + ].subscribe.toString()})` + ); + } + this.log.debug('complete Custom: ' + JSON.stringify(custom)); + + if (this.config.subscriptions) { + for (const topic of this.config.subscriptions.split(',')) { + if (topic && topic.trim()) { + addTopics[topic.trim()] = 0; // QoS + } + } + } + this.log.debug(`found ${Object.keys(addTopics).length} additional topic to subscribe to`); + + let will = undefined; + + if (this.config.lastWillTopic && this.config.lastWillMessage) { + this.log.info( + `Try to connect to ${__url}, protocol version ${this.config.mqttVersion} with lwt "${this.config.lastWillTopic}"` + ); + + will = { + topic: this.topicAddPrefixOut(this.config.lastWillTopic), + payload: this.config.lastWillMessage, + qos: 2, + retain: true + }; + } else { + this.log.info('Try to connect to ' + __url); + } + const mqttVersion = Number.parseInt(this.config.mqttVersion || 4); + try { + this.client = mqtt.connect(_url, { + host: this.config.host, + port: this.config.port, + protocolVersion: mqttVersion, + ssl: this.config.ssl, + rejectUnauthorized: this.config.rejectUnauthorized, + reconnectPeriod: this.config.reconnectPeriod, + username: this.config.username, + password: this.config.password, + clientId: this.config.clientId, + clean: true, + will + }); + } catch (e) { + this.log.error(e); + this.finish(() => { + setTimeout(() => (this.terminate ? this.terminate() : process.exit(0)), 200); + }); + return; + } + + this.client.on('connect', this.connect.bind(this)); + this.client.on('reconnect', this.reconnect.bind(this)); + this.client.on('disconnect', this.disconnect.bind(this)); + this.client.on('offline', this.offline.bind(this)); + this.client.on('message', this.message.bind(this)); + this.client.on('error', this.error.bind(this)); + }); + }); + } + + this.subscribeForeignObjects('*'); + }); + } + + /** + * Is called when databases are this. and adapter received configuration. + */ + onReady() { + this.main(); + } + + /** + * Is called when adapter shuts down - callback has to be called under any circumstances! + * @param {() => void} callback + */ + finish(callback) { + if (this.adapterFinished) { + return; + } + if (this.client && this.config.onDisconnectTopic && this.config.onDisconnectMessage) { + const topic = this.config.onDisconnectTopic; + + this.log.info(`Disconnecting with message "${this.config.onDisconnectMessage}" on topic "${topic}"`); + this.client.publish( + this.topicAddPrefixOut(topic), + this.config.onDisconnectMessage, + { qos: 2, retain: true }, + () => { + this.log.debug( + 'successfully published ' + + JSON.stringify({ topic: topic, message: this.config.onDisconnectMessage }) + ); + this.end(callback); + } + ); + } else { + this.end(callback); + } + } + + /** + * Is called when adapter shuts down - callback has to be called under any circumstances! + * @param {() => void} callback + */ + end(callback) { + this.adapterFinished = true; + this.client && + this.client.end(() => { + this.log.debug(`closed client`); + this.setState('info.connection', false, true); + callback && callback(); + }); + } + + /** + * Is called when adapter shuts down - callback has to be called under any circumstances! + * @param {() => void} callback + */ + onUnload(callback) { + try { + this.finish(callback); + } catch { + if (callback) { + callback(); + } + } + } + + /** + * Is called if a subscribed object changes + * @param {string} id + * @param {ioBroker.Object | null | undefined} obj + */ + onObjectChange(id, obj) { + const custom = _context.custom; + const subTopics = _context.subTopics; + const topic2id = _context.topic2id; + + if (obj?.common?.custom?.[this.namespace]?.enabled) { + custom[id] = obj.common.custom[this.namespace]; + custom[id].type = obj.common.type; + + this.checkSettings(id, custom[id], this.namespace, this.config.qos, this.config.subQos); + + if (custom[id].subscribe) { + subTopics[custom[id].topic] = custom[id].subQos; + topic2id[custom[id].topic] = id; + const sub = {}; + sub[custom[id].topic] = custom[id].subQos; + + this.subscribe(sub, () => { + this.log.debug('subscribed to ' + JSON.stringify(sub)); + }); + } else { + delete subTopics[custom[id].topic]; + delete topic2id[custom[id].topic]; + this.iobUnsubscribe(id); + + this.unsubscribe( + custom[id].topic, + () => custom[id] && this.log.debug('unsubscribed from ' + custom[id].topic) + ); + } + + if (custom[id].enabled) { + //@todo should this be .subscribe? + //subscribe to state changes + this.iobSubscribe(id, err => { + //publish state once + if (err || !custom[id].publish) { + return; + } + this.getForeignState(id, (err, state) => { + if (err || !state) { + return; + } + this.log.debug(`publish ${id} once: ${JSON.stringify(state)}`); + this.onStateChange(id, state); + }); + }); + } + + this.log.debug( + `enabled syncing of ${id} (publish/subscribe:${custom[id].publish.toString()}/${custom[ + id + ].subscribe.toString()})` + ); + } else if (custom[id]) { + const topic = custom[id].topic; + + this.unsubscribe(topic, () => this.log.debug('unsubscribed from ' + topic)); + + delete subTopics[custom[id].topic]; + delete topic2id[custom[id].topic]; + + if (custom[id].publish) { + this.iobUnsubscribe(id); + } + + delete custom[id]; + + this.log.debug('disabled syncing of ' + id); + } + } + + /** + * Is called if a subscribed state changes + * @param {string} id + * @param {ioBroker.State | null | undefined} state + */ + onStateChange(id, state) { + const custom = _context.custom; + + if (custom[id]) { + custom[id].state = state; + + if (custom[id].enabled && custom[id].publish) { + if (!state) { + // The state was deleted/expired, make sure it is no longer retained + this.unpublish(id); + } else if (state.from !== 'system.adapter.' + this.namespace) { + // prevent republishing to same broker + this.publishState(id, state); + } + } + } + } + + /** + * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... + * Using this method requires "common.message" property to be set to true in io-package.json + * @param {ioBroker.Message} obj + */ + onMessage(obj) { + if (typeof obj === 'object' && obj.command) { + if (obj.command === 'stopInstance') { + // e.g. send email or pushover or whatever + this.log.info('Stop Instance command received...'); + + this.finish(() => { + this.sendTo(obj.from, obj.command, 'Message received', obj.callback); + setTimeout(() => (this.terminate ? this.terminate() : process.exit(0)), 200); + }); + // Send response in callback if required + // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback); + } + } + } } // @ts-ignore parent is a valid property on module if (module.parent) { - // Export the constructor in compact mode - /** - * @param {Partial} [options={}] - */ - module.exports = (options) => new MqttClient(options); + // Export the constructor in compact mode + /** + * @param {Partial} [options={}] + */ + module.exports = options => new MqttClient(options); } else { - // otherwise start the instance directly - new MqttClient(); + // otherwise start the instance directly + new MqttClient(); } diff --git a/package.json b/package.json index b92c863..d5a2bde 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,14 @@ "node": ">=16.0.0" }, "dependencies": { - "@iobroker/adapter-core": "^2.6.8", + "@iobroker/adapter-core": "^3.0.3", "mqtt": "^4.3.7" }, "devDependencies": { "@alcalzone/release-script": "^3.6.0", "@alcalzone/release-script-plugin-iobroker": "^3.6.0", "@alcalzone/release-script-plugin-license": "^3.5.9", + "@foxriver76/eslint-config": "^1.0.1", "@iobroker/adapter-dev": "^1.2.0", "@iobroker/testing": "^4.1.0", "@types/chai": "^4.3.5", diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..adbbdcd --- /dev/null +++ b/prettier.config.js @@ -0,0 +1 @@ +module.exports = require('@foxriver76/eslint-config/prettier'); \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js index f8aa782..22cdc50 100644 --- a/test/lib/setup.js +++ b/test/lib/setup.js @@ -1,12 +1,12 @@ -/* jshint -W097 */// jshint strict:false +/* jshint -W097 */ // jshint strict:false /*jslint node: true */ // check if tmp directory exists -const fs = require('fs'); -const path = require('path'); +const fs = require('fs'); +const path = require('path'); const child_process = require('child_process'); -const rootDir = path.normalize(__dirname + '/../../'); -const pkg = require(rootDir + 'package.json'); -const debug = typeof v8debug === 'object'; +const rootDir = path.normalize(__dirname + '/../../'); +const pkg = require(rootDir + 'package.json'); +const debug = typeof v8debug === 'object'; pkg.main = pkg.main || 'main.js'; let JSONLDB; @@ -45,21 +45,19 @@ let pid = null; let systemConfig = null; function copyFileSync(source, target) { - let targetFile = target; //if target is a directory a new file with the same name will be created if (fs.existsSync(target)) { - if ( fs.lstatSync( target ).isDirectory() ) { + if (fs.lstatSync(target).isDirectory()) { targetFile = path.join(target, path.basename(source)); } } try { fs.writeFileSync(targetFile, fs.readFileSync(source)); - } - catch (err) { - console.log('file copy error: ' +source +' -> ' + targetFile + ' (error ignored)'); + } catch (err) { + console.log('file copy error: ' + source + ' -> ' + targetFile + ' (error ignored)'); } } @@ -88,9 +86,15 @@ function copyFolderRecursiveSync(source, target, ignore) { const curTarget = path.join(targetFolder, file); if (fs.lstatSync(curSource).isDirectory()) { // ignore grunt files - if (file.indexOf('grunt') !== -1) return; - if (file === 'chai') return; - if (file === 'mocha') return; + if (file.indexOf('grunt') !== -1) { + return; + } + if (file === 'chai') { + return; + } + if (file === 'mocha') { + return; + } copyFolderRecursiveSync(curSource, targetFolder, ignore); } else { copyFileSync(curSource, curTarget); @@ -201,7 +205,9 @@ async function checkIsAdapterInstalled(cb, counter, customName) { if (objects['system.adapter.' + customName + '.0']) { console.log('checkIsAdapterInstalled: ready!'); setTimeout(function () { - if (cb) cb(); + if (cb) { + cb(); + } }, 100); return; } else { @@ -225,7 +231,9 @@ async function checkIsAdapterInstalled(cb, counter, customName) { if (obj) { console.log('checkIsAdapterInstalled: ready!'); setTimeout(function () { - if (cb) cb(); + if (cb) { + cb(); + } }, 100); return; } else { @@ -234,17 +242,18 @@ async function checkIsAdapterInstalled(cb, counter, customName) { } else { console.error('checkIsAdapterInstalled: No objects file found in datadir ' + dataDir); } - } catch (err) { console.log('checkIsAdapterInstalled: catch ' + err); } if (counter > 20) { console.error('checkIsAdapterInstalled: Cannot install!'); - if (cb) cb('Cannot install'); + if (cb) { + cb('Cannot install'); + } } else { console.log('checkIsAdapterInstalled: wait...'); - setTimeout(function() { + setTimeout(function () { checkIsAdapterInstalled(cb, counter + 1); }, 1000); } @@ -262,7 +271,9 @@ async function checkIsControllerInstalled(cb, counter) { if (objects['system.certificates']) { console.log('checkIsControllerInstalled: installed!'); setTimeout(function () { - if (cb) cb(); + if (cb) { + cb(); + } }, 100); return; } @@ -284,24 +295,25 @@ async function checkIsControllerInstalled(cb, counter) { if (obj) { console.log('checkIsControllerInstalled: installed!'); setTimeout(function () { - if (cb) cb(); + if (cb) { + cb(); + } }, 100); return; } - } else { console.error('checkIsControllerInstalled: No objects file found in datadir ' + dataDir); } - } catch (err) { - - } + } catch (err) {} if (counter > 20) { console.log('checkIsControllerInstalled: Cannot install!'); - if (cb) cb('Cannot install'); + if (cb) { + cb('Cannot install'); + } } else { console.log('checkIsControllerInstalled: wait...'); - setTimeout(function() { + setTimeout(function () { checkIsControllerInstalled(cb, counter + 1); }, 1000); } @@ -318,26 +330,34 @@ function installAdapter(customName, cb) { // make first install if (debug) { child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { - cwd: rootDir + 'tmp', + cwd: rootDir + 'tmp', stdio: [0, 1, 2] }); checkIsAdapterInstalled(function (error) { - if (error) console.error(error); + if (error) { + console.error(error); + } console.log('Adapter installed.'); - if (cb) cb(); + if (cb) { + cb(); + } }); } else { // add controller const _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { - cwd: rootDir + 'tmp', + cwd: rootDir + 'tmp', stdio: [0, 1, 2, 'ipc'] }); waitForEnd(_pid, function () { checkIsAdapterInstalled(function (error) { - if (error) console.error(error); + if (error) { + console.error(error); + } console.log('Adapter installed.'); - if (cb) cb(); + if (cb) { + cb(); + } }); }); } @@ -364,12 +384,20 @@ function waitForEnd(_pid, cb) { function installJsController(cb) { console.log('installJsController...'); - if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || - !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + if ( + !fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data') + ) { // try to detect appName.js-controller in node_modules/appName.js-controller // travis CI installs js-controller into node_modules if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { - console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + console.log( + 'installJsController: no js-controller => copy it from "' + + rootDir + + 'node_modules/' + + appName + + '.js-controller"' + ); // copy all // stop controller console.log('Stop controller if running...'); @@ -382,19 +410,26 @@ function installJsController(cb) { }); } else { _pid = child_process.fork(appName + '.js', ['stop'], { - cwd: rootDir + 'node_modules/' + appName + '.js-controller', + cwd: rootDir + 'node_modules/' + appName + '.js-controller', stdio: [0, 1, 2, 'ipc'] }); } waitForEnd(_pid, function () { // copy all files into - if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); - if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); + } + if (!fs.existsSync(rootDir + 'tmp/node_modules')) { + fs.mkdirSync(rootDir + 'tmp/node_modules'); + } - if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { console.log('Copy js-controller...'); - copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + copyFolderRecursiveSync( + rootDir + 'node_modules/' + appName + '.js-controller', + rootDir + 'tmp/node_modules/' + ); } console.log('Setup js-controller...'); @@ -407,7 +442,7 @@ function installJsController(cb) { }); } else { __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { - cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', stdio: [0, 1, 2, 'ipc'] }); } @@ -416,19 +451,24 @@ function installJsController(cb) { // change ports for object and state DBs const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); config.objects.port = 19001; - config.states.port = 19000; + config.states.port = 19000; // TEST WISE! //config.objects.type = 'jsonl'; //config.states.type = 'jsonl'; - fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + fs.writeFileSync( + rootDir + 'tmp/' + appName + '-data/' + appName + '.json', + JSON.stringify(config, null, 2) + ); console.log('Setup finished.'); copyAdapterToController(); installAdapter(async function () { await storeOriginalFiles(); - if (cb) cb(true); + if (cb) { + cb(true); + } }); }); }); @@ -437,8 +477,10 @@ function installJsController(cb) { // check if port 9000 is free, else admin adapter will be added to running instance const client = new require('net').Socket(); client.on('error', () => {}); - client.connect(9000, '127.0.0.1', function() { - console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + client.connect(9000, '127.0.0.1', function () { + console.error( + 'Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.' + ); process.exit(0); }); @@ -448,7 +490,7 @@ function installJsController(cb) { console.log('installJsController: no js-controller => install dev build from npm'); child_process.execSync('npm install ' + appName + '.js-controller@dev --prefix ./ --production', { - cwd: rootDir + 'tmp/', + cwd: rootDir + 'tmp/', stdio: [0, 1, 2] }); } else { @@ -462,7 +504,7 @@ function installJsController(cb) { }); } else { child_process.fork(appName + '.js', ['setup', 'first'], { - cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', stdio: [0, 1, 2, 'ipc'] }); } @@ -474,7 +516,7 @@ function installJsController(cb) { if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { _pid = child_process.fork(appName + '.js', ['stop'], { - cwd: rootDir + 'node_modules/' + appName + '.js-controller', + cwd: rootDir + 'node_modules/' + appName + '.js-controller', stdio: [0, 1, 2, 'ipc'] }); } @@ -483,18 +525,23 @@ function installJsController(cb) { // change ports for object and state DBs const config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); config.objects.port = 19001; - config.states.port = 19000; + config.states.port = 19000; // TEST WISE! //config.objects.type = 'jsonl'; //config.states.type = 'jsonl'; - fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + fs.writeFileSync( + rootDir + 'tmp/' + appName + '-data/' + appName + '.json', + JSON.stringify(config, null, 2) + ); copyAdapterToController(); installAdapter(async function () { await storeOriginalFiles(); - if (cb) cb(true); + if (cb) { + cb(true); + } }); }); }); @@ -503,7 +550,9 @@ function installJsController(cb) { } else { setTimeout(function () { console.log('installJsController: js-controller installed'); - if (cb) cb(false); + if (cb) { + cb(false); + } }, 0); } } @@ -511,7 +560,13 @@ function installJsController(cb) { function copyAdapterToController() { console.log('Copy adapter...'); // Copy adapter to tmp/node_modules/appName.adapter - copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', [ + '.idea', + 'test', + 'tmp', + '.git', + appName + '.js-controller' + ]); console.log('Adapter copied.'); } @@ -527,7 +582,7 @@ function clearControllerLog() { files = []; fs.mkdirSync(dirPath); } - } catch(e) { + } catch (e) { console.error('Cannot read "' + dirPath + '"'); return; } @@ -556,7 +611,7 @@ function clearDB() { files = []; fs.mkdirSync(dirPath); } - } catch(e) { + } catch (e) { console.error('Cannot read "' + dirPath + '"'); return; } @@ -593,25 +648,29 @@ function setupController(cb) { objs = JSON.parse(objs); } catch (e) { console.log('ERROR reading/parsing system configuration. Ignore'); - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } if (!objs || !objs['system.config']) { - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } systemConfig = objs['system.config']; - if (cb) cb(objs['system.config']); + if (cb) { + cb(objs['system.config']); + } } else if (fs.existsSync(dataDir + 'objects.jsonl')) { loadJSONLDB(); const db = new JSONLDB(dataDir + 'objects.jsonl'); await db.open(); - let config = db.get('system.config'); + const config = db.get('system.config'); systemConfig = config || {}; await db.close(); - if (cb) cb(systemConfig); + if (cb) { + cb(systemConfig); + } } else { console.error('read SystemConfig: No objects file found in datadir ' + dataDir); } @@ -622,7 +681,7 @@ function setupController(cb) { } async function getSecret() { - var dataDir = rootDir + 'tmp/' + appName + '-data/'; + const dataDir = rootDir + 'tmp/' + appName + '-data/'; if (systemConfig) { return systemConfig.native.secret; @@ -632,13 +691,12 @@ async function getSecret() { try { objs = fs.readFileSync(dataDir + 'objects.json'); objs = JSON.parse(objs); - } - catch (e) { - console.warn("Could not load secret. Reason: " + e); + } catch (e) { + console.warn('Could not load secret. Reason: ' + e); return null; } if (!objs || !objs['system.config']) { - objs = {'system.config': {}}; + objs = { 'system.config': {} }; } return objs['system.config'].native.secre; @@ -656,12 +714,11 @@ async function getSecret() { } else { console.error('read secret: No objects file found in datadir ' + dataDir); } - } -function encrypt (key, value) { - var result = ''; - for (var i = 0; i < value.length; ++i) { +function encrypt(key, value) { + let result = ''; + for (let i = 0; i < value.length; ++i) { result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); } return result; @@ -670,7 +727,9 @@ function encrypt (key, value) { function startAdapter(objects, states, callback) { if (adapterStarted) { console.log('Adapter already started ...'); - if (callback) callback(objects, states); + if (callback) { + callback(objects, states); + } return; } adapterStarted = true; @@ -686,7 +745,7 @@ function startAdapter(objects, states, callback) { } else { // start controller pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { - cwd: rootDir + 'tmp', + cwd: rootDir + 'tmp', stdio: [0, 1, 2, 'ipc'] }); } @@ -696,7 +755,9 @@ function startAdapter(objects, states, callback) { } else { console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); } - if (callback) callback(objects, states); + if (callback) { + callback(objects, states); + } } function startController(isStartAdapter, onObjectChange, onStateChange, callback) { @@ -708,7 +769,7 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback } if (onStateChange === undefined) { - callback = onObjectChange; + callback = onObjectChange; onObjectChange = undefined; } @@ -725,19 +786,23 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback // rootDir + 'tmp/node_modules const objPath = require.resolve(`@iobroker/db-objects-${config.objects.type}`, { - paths: [ rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller'] + paths: [ + rootDir + 'tmp/node_modules', + rootDir, + rootDir + 'tmp/node_modules/' + appName + '.js-controller' + ] }); console.log('Objects Path: ' + objPath); const Objects = require(objPath).Server; objects = new Objects({ connection: { - 'type': config.objects.type, - 'host': '127.0.0.1', - 'port': 19001, - 'user': '', - 'pass': '', - 'noFileCache': false, - 'connectTimeout': 2000 + type: config.objects.type, + host: '127.0.0.1', + port: 19001, + user: '', + pass: '', + noFileCache: false, + connectTimeout: 2000 }, logger: { silly: function (msg) { @@ -775,7 +840,11 @@ function startController(isStartAdapter, onObjectChange, onStateChange, callback // Just open in memory DB itself const statePath = require.resolve(`@iobroker/db-states-${config.states.type}`, { - paths: [ rootDir + 'tmp/node_modules', rootDir, rootDir + 'tmp/node_modules/' + appName + '.js-controller'] + paths: [ + rootDir + 'tmp/node_modules', + rootDir, + rootDir + 'tmp/node_modules/' + appName + '.js-controller' + ] }); console.log('States Path: ' + statePath); const States = require(statePath).Server; @@ -841,14 +910,18 @@ function stopAdapter(cb) { pid.on('exit', function (code, signal) { if (pid) { console.log('child process terminated due to receipt of signal ' + signal); - if (cb) cb(); + if (cb) { + cb(); + } pid = null; } }); pid.on('close', function (code, signal) { if (pid) { - if (cb) cb(); + if (cb) { + cb(); + } pid = null; } }); @@ -873,7 +946,7 @@ function stopController(cb) { if (objects) { console.log('Set system.adapter.' + pkg.name + '.0'); objects.setObject('system.adapter.' + pkg.name + '.0', { - common:{ + common: { enabled: false } }); @@ -912,17 +985,25 @@ async function setAdapterConfig(common, native, instance) { const id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); if (fs.existsSync(rootDir + 'tmp/' + appName + '-data/objects.json')) { const objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); - if (common) objects[id].common = common; - if (native) objects[id].native = native; + if (common) { + objects[id].common = common; + } + if (native) { + objects[id].native = native; + } fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); } else if (fs.existsSync(rootDir + 'tmp/' + appName + '-data/objects.jsonl')) { loadJSONLDB(); const db = new JSONLDB(rootDir + 'tmp/' + appName + '-data/objects.jsonl'); await db.open(); - let obj = db.get(id); - if (common) obj.common = common; - if (native) obj.native = native; + const obj = db.get(id); + if (common) { + obj.common = common; + } + if (native) { + obj.native = native; + } db.set(id, obj); await db.close(); @@ -942,7 +1023,7 @@ async function getAdapterConfig(instance) { const db = new JSONLDB(rootDir + 'tmp/' + appName + '-data/objects.jsonl'); await db.open(); - let obj = db.get(id); + const obj = db.get(id); await db.close(); return obj; @@ -954,15 +1035,15 @@ async function getAdapterConfig(instance) { if (typeof module !== undefined && module.parent) { module.exports.getAdapterConfig = getAdapterConfig; module.exports.setAdapterConfig = setAdapterConfig; - module.exports.startController = startController; - module.exports.stopController = stopController; - module.exports.setupController = setupController; - module.exports.stopAdapter = stopAdapter; - module.exports.startAdapter = startAdapter; - module.exports.installAdapter = installAdapter; - module.exports.appName = appName; - module.exports.adapterName = adapterName; - module.exports.adapterStarted = adapterStarted; - module.exports.getSecret = getSecret; - module.exports.encrypt = encrypt; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; + module.exports.getSecret = getSecret; + module.exports.encrypt = encrypt; } diff --git a/test/mocha.setup.js b/test/mocha.setup.js index 2adcb98..f641040 100644 --- a/test/mocha.setup.js +++ b/test/mocha.setup.js @@ -1 +1,3 @@ -process.on("unhandledRejection", (r) => { throw r; }); +process.on('unhandledRejection', r => { + throw r; +}); diff --git a/test/testStartup.js b/test/testStartup.js index 267d207..7a8f2c9 100644 --- a/test/testStartup.js +++ b/test/testStartup.js @@ -1,5 +1,5 @@ const path = require('path'); -const {tests, utils} = require('@iobroker/testing'); +const { tests, utils } = require('@iobroker/testing'); const { expect } = require('chai'); // Run tests @@ -26,7 +26,9 @@ tests.unit(path.join(__dirname, '..'), { } } }, - native: {/* ... */}, + native: { + /* ... */ + } }, { _id: 'test.0.object2', @@ -41,15 +43,17 @@ tests.unit(path.join(__dirname, '..'), { } } }, - native: {/* ... */}, + native: { + /* ... */ + } } ], // Optionally define which states need to exist in the states DB // You can set all properties that are usually available on a state predefinedStates: { - 'test.0.object1': {val: true, ack: false}, - 'test.0.object2': {val: 2, ack: false, ts: 1}, + 'test.0.object1': { val: true, ack: false }, + 'test.0.object2': { val: 2, ack: false, ts: 1 } }, // If the startup tests need require specific behavior of the mocks @@ -57,25 +61,29 @@ tests.unit(path.join(__dirname, '..'), { // you can define the behavior here. This method is called before every predefined test defineMockBehavior(database, adapter) { adapter.getObjectView = (schema, type, options, callback) => - callback && callback(null, [{ - id: 'test.0.object1', - value: { - 'mqtt-client.0': { - enabled: true, - publish: true, - subscribe: true + callback && + callback(null, [ + { + id: 'test.0.object1', + value: { + 'mqtt-client.0': { + enabled: true, + publish: true, + subscribe: true + } } - } - }, { - id: 'test.0.object2', - value: { - 'mqtt-client.0': { - enabled: true, - publish: true, - subscribe: true + }, + { + id: 'test.0.object2', + value: { + 'mqtt-client.0': { + enabled: true, + publish: true, + subscribe: true + } } } - }]); + ]); // or adapter.objects.getUserGroup.returns('a string'); }, @@ -84,7 +92,7 @@ tests.unit(path.join(__dirname, '..'), { // If you need predefined objects etc. here, you need to take care of it yourself defineAdditionalTests() { // Create mocks and asserts - const { adapter, database } = utils.unit.createMocks(); + const { adapter, database } = utils.unit.createMocks(); const { assertObjectExists } = utils.unit.createAsserts(database, adapter); describe('test start', () => { @@ -98,11 +106,11 @@ tests.unit(path.join(__dirname, '..'), { it('works', () => { // Create an object in the fake db we will use in this test const theObject = { - _id: "test.0.whatever", - type: "state", + _id: 'test.0.whatever', + type: 'state', common: { - role: "whatever", - }, + role: 'whatever' + } }; database.publishObject(theObject); @@ -113,5 +121,5 @@ tests.unit(path.join(__dirname, '..'), { expect(database.hasObject(theObject._id)).to.be.true; }); }); - }, -}); \ No newline at end of file + } +});