376 lines
17 KiB
JavaScript
376 lines
17 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const mqtt = require('mqtt');
|
||
|
const utils = require(__dirname + '/utils');
|
||
|
const tools = require(require(__dirname + '/utils').controllerDir + '/lib/tools');
|
||
|
const state2string = require(__dirname + '/common').state2string;
|
||
|
const convertTopic2id = require(__dirname + '/common').convertTopic2id;
|
||
|
const convertID2topic = require(__dirname + '/common').convertID2topic;
|
||
|
|
||
|
const messageboxRegex = new RegExp('\\.messagebox$');
|
||
|
|
||
|
function MQTTClient(adapter, states) {
|
||
|
if (!(this instanceof MQTTClient)) return new MQTTClient(adapter, states);
|
||
|
|
||
|
let client = null;
|
||
|
let topic2id = {};
|
||
|
let id2topic = {};
|
||
|
const namespaceRegEx = new RegExp('^' + adapter.namespace.replace('.', '\\.') + '\\.');
|
||
|
let connected = false;
|
||
|
|
||
|
this.destroy = () => {
|
||
|
if (client) {
|
||
|
client.end();
|
||
|
client = null;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.onStateChange = (id, state) => send2Server(id, state);
|
||
|
|
||
|
function send2Server(id, state, cb) {
|
||
|
if (!client) return;
|
||
|
const topic = id2topic[id];
|
||
|
adapter.log.info('send2Server ' + id + '[' + topic + ']');
|
||
|
if (!topic) {
|
||
|
adapter.getForeignObject(id, (err, obj) => {
|
||
|
if (!client) return;
|
||
|
if (!obj) {
|
||
|
adapter.log.warn('Cannot resolve topic name for ID: ' + id + ' (object not found)');
|
||
|
if (cb) cb(id);
|
||
|
return;
|
||
|
} else if (!obj.native || !obj.native.topic) {
|
||
|
id2topic[obj._id] = convertID2topic(obj._id, null, adapter.config.prefix, adapter.namespace);
|
||
|
} else {
|
||
|
id2topic[obj._id] = obj.native.topic;
|
||
|
}
|
||
|
send2Server(obj._id, state, cb);
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
if (!state) {
|
||
|
if (adapter.config.debug) adapter.log.debug('Send to server "' + topic + '": deleted');
|
||
|
client.publish(topic, null, {qos: adapter.config.defaultQoS, retain: adapter.config.retain});
|
||
|
} else {
|
||
|
const s = state2string(state.val);
|
||
|
if (adapter.config.debug) adapter.log.debug('Send to server "' + adapter.config.prefix + topic + '": ' + s);
|
||
|
|
||
|
if (adapter.config.extraSet && state && !state.ack) {
|
||
|
client.publish(topic + '/set', s, {qos: adapter.config.defaultQoS, retain: adapter.config.retain});
|
||
|
} else {
|
||
|
//if (s > 0) client.publish(topic, s, {qos: adapter.config.defaultQoS, retain: adapter.config.retain});
|
||
|
client.publish(topic, s, {qos: adapter.config.defaultQoS, retain: adapter.config.retain});
|
||
|
}
|
||
|
}
|
||
|
if (cb) cb(id);
|
||
|
}
|
||
|
|
||
|
function publishAllStates(config, toPublish) {
|
||
|
if (!toPublish || !toPublish.length) {
|
||
|
adapter.log.info('All states published');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!client) return;
|
||
|
|
||
|
const id = toPublish[0];
|
||
|
if (!id2topic[id]) {
|
||
|
adapter.getForeignObject(id, (err, obj) => {
|
||
|
if (!client) return;
|
||
|
if (!obj) {
|
||
|
adapter.log.warn('Cannot resolve topic name for ID: "' + id + '" (object not found)');
|
||
|
return;
|
||
|
} else if (!obj.native || !obj.native.topic) {
|
||
|
id2topic[obj._id] = convertID2topic(obj._id, null, config.prefix, adapter.namespace);
|
||
|
} else {
|
||
|
id2topic[obj._id] = obj.native.topic;
|
||
|
}
|
||
|
setImmediate(() => publishAllStates(config, toPublish));
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
toPublish.shift();
|
||
|
|
||
|
if (adapter.config.extraSet && states[id] && !states[id].ack) {
|
||
|
client.publish(id2topic[id] + '/set', state2string(states[id].val), {qos: adapter.config.defaultQoS, retain: adapter.config.retain}, err => {
|
||
|
if (err) adapter.log.error('client.publish2: ' + err);
|
||
|
setImmediate(() => publishAllStates(config, toPublish));
|
||
|
});
|
||
|
} else {
|
||
|
if (states[id]) {
|
||
|
client.publish(id2topic[id], state2string(states[id].val), {qos: adapter.config.defaultQoS, retain: adapter.config.retain}, err => {
|
||
|
if (err) adapter.log.error('client.publish: ' + err);
|
||
|
setImmediate(() => publishAllStates(config, toPublish));
|
||
|
});
|
||
|
} else {
|
||
|
setImmediate(() => publishAllStates(config, toPublish));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
(function _constructor(config) {
|
||
|
const clientId = config.clientId || ((tools.getHostname ? tools.getHostname() : utils.appName) + '.' + adapter.namespace);
|
||
|
const _url = ((!config.ssl) ? 'mqtt' : 'mqtts') + '://' + (config.user ? (config.user + ':' + config.pass + '@') : '') + config.url + (config.port ? (':' + config.port) : '') + '?clientId=' + clientId;
|
||
|
const __url = ((!config.ssl) ? 'mqtt' : 'mqtts') + '://' + (config.user ? (config.user + ':*******************@') : '') + config.url + (config.port ? (':' + config.port) : '') + '?clientId=' + clientId;
|
||
|
adapter.log.info('Try to connect to ' + __url);
|
||
|
client = mqtt.connect(_url, {
|
||
|
keepalive: config.keepalive || 10, /* in seconds */
|
||
|
protocolId: 'MQTT',
|
||
|
protocolVersion: 4,
|
||
|
reconnectPeriod: config.reconnectPeriod || 1000, /* in milliseconds */
|
||
|
connectTimeout: (config.connectTimeout || 30) * 1000, /* in milliseconds */
|
||
|
clean: config.clean === undefined ? true : config.clean
|
||
|
});
|
||
|
|
||
|
// By default subscribe on all topics
|
||
|
if (!config.patterns) config.patterns = '#';
|
||
|
|
||
|
if (typeof config.patterns === 'string') {
|
||
|
config.patterns = config.patterns.split(',');
|
||
|
}
|
||
|
|
||
|
// create connected object and state
|
||
|
adapter.getObject('info.connection', (err, obj) => {
|
||
|
if (!obj || !obj.common || obj.common.type !== 'boolean') {
|
||
|
obj = {
|
||
|
_id: 'info.connection',
|
||
|
type: 'state',
|
||
|
common: {
|
||
|
role: 'indicator.connected',
|
||
|
name: 'If connected to MQTT broker',
|
||
|
type: 'boolean',
|
||
|
read: true,
|
||
|
write: false,
|
||
|
def: false
|
||
|
},
|
||
|
native: {}
|
||
|
};
|
||
|
|
||
|
adapter.setObject('info.connection', obj, () => adapter.setState('info.connection', connected, true));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// topic from MQTT broker received
|
||
|
client.on('message', (topic, message) => {
|
||
|
if (!topic) return;
|
||
|
|
||
|
let isAck = true;
|
||
|
|
||
|
if (adapter.config.extraSet) {
|
||
|
if (topic.match(/\/set$/)) {
|
||
|
isAck = false;
|
||
|
topic = topic.substring(0, topic.length - 4);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// try to convert topic to ID
|
||
|
let id = (topic2id[topic] && topic2id[topic].id) || convertTopic2id(topic, false, config.prefix, adapter.namespace);
|
||
|
|
||
|
if (id.length > config.maxTopicLength) {
|
||
|
adapter.log.warn('[' + client.id + '] Topic name is too long: ' + id.substring(0, 100) + '...');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof message === 'object') {
|
||
|
message = message.toString();
|
||
|
}
|
||
|
|
||
|
if (typeof message === 'string') {
|
||
|
// Try to convert value
|
||
|
let _val = message.replace(',', '.').replace(/^\+/, '');
|
||
|
|
||
|
// +23.560 => 23.56, -23.000 => -23
|
||
|
if (_val.indexOf('.') !== -1) {
|
||
|
let i = _val.length - 1;
|
||
|
while (_val[i] === '0' || _val[i] === '.') {
|
||
|
i--;
|
||
|
if (_val[i + 1] === '.') break;
|
||
|
}
|
||
|
if (_val[i + 1] === '0' || _val[i + 1] === '.') {
|
||
|
_val = _val.substring(0, i + 1);
|
||
|
}
|
||
|
}
|
||
|
const f = parseFloat(_val);
|
||
|
|
||
|
if (f.toString() === _val) message = f;
|
||
|
if (message === 'true') message = true;
|
||
|
if (message === 'false') message = false;
|
||
|
}
|
||
|
|
||
|
if (config.debug) {
|
||
|
adapter.log.debug('Server publishes "' + topic + '": ' + message);
|
||
|
}
|
||
|
|
||
|
if (typeof message === 'string' && message[0] === '{') {
|
||
|
try {
|
||
|
const _message = JSON.parse(message);
|
||
|
if (_message.val !== undefined && _message.ack !== undefined) {
|
||
|
message = _message;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
adapter.log.warn('Cannot parse "' + topic + '": ' + message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if no cache for this topic found
|
||
|
if (!topic2id[topic]) {
|
||
|
topic2id[topic] = {id: null, isAck: isAck, message: message};
|
||
|
|
||
|
// Create object if not exists
|
||
|
adapter.getObject(id, (err, obj) => {
|
||
|
if (!obj) {
|
||
|
adapter.getForeignObject(id, (err, obj) => {
|
||
|
if (!obj) {
|
||
|
// create state
|
||
|
obj = {
|
||
|
common: {
|
||
|
name: topic,
|
||
|
write: true,
|
||
|
read: true,
|
||
|
role: 'variable',
|
||
|
desc: 'mqtt client variable',
|
||
|
type: typeof topic2id[topic].message
|
||
|
},
|
||
|
native: {
|
||
|
topic: topic
|
||
|
},
|
||
|
type: 'state'
|
||
|
};
|
||
|
|
||
|
if (obj.common.type === 'object' && topic2id[topic].message.val !== undefined) {
|
||
|
obj.common.type = typeof topic2id[topic].message.val;
|
||
|
}
|
||
|
id = adapter.namespace + '.' + id;
|
||
|
topic2id[topic].id = id;
|
||
|
id2topic[id] = topic;
|
||
|
|
||
|
adapter.log.debug('Create object for topic: ' + topic + '[ID: ' + topic2id[topic].id + ']');
|
||
|
|
||
|
adapter.setForeignObject(topic2id[topic].id, obj, err => err && adapter.log.error('setForeignObject: ' + err));
|
||
|
|
||
|
if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message);
|
||
|
|
||
|
// write
|
||
|
if (typeof topic2id[topic].message === 'object') {
|
||
|
adapter.setForeignState(topic2id[topic].id, topic2id[topic].message);
|
||
|
} else {
|
||
|
adapter.setForeignState(topic2id[topic].id, {val: topic2id[topic].message, ack: topic2id[topic].isAck});
|
||
|
}
|
||
|
} else {
|
||
|
// expand old version of objects
|
||
|
if (namespaceRegEx.test(obj._id) && (!obj.native || !obj.native.topic)) {
|
||
|
obj.native = obj.native || {};
|
||
|
obj.native.topic = topic;
|
||
|
adapter.setForeignObject(obj._id, obj);
|
||
|
}
|
||
|
// this is topic from other adapter
|
||
|
topic2id[topic].id = id;
|
||
|
id2topic[topic2id[topic].id] = topic;
|
||
|
|
||
|
if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message);
|
||
|
|
||
|
if (typeof topic2id[topic].message === 'object') {
|
||
|
adapter.setForeignState(topic2id[topic].id, topic2id[topic].message);
|
||
|
} else {
|
||
|
adapter.setForeignState(topic2id[topic].id, {val: topic2id[topic].message, ack: topic2id[topic].isAck});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
// expand old version of objects
|
||
|
if (namespaceRegEx.test(obj._id) && (!obj.native || !obj.native.topic)) {
|
||
|
obj.native = obj.native || {};
|
||
|
obj.native.topic = topic;
|
||
|
adapter.setForeignObject(obj._id, obj, err => err && adapter.log.error('setForeignObject2: ' + err));
|
||
|
}
|
||
|
|
||
|
// this is topic from this adapter
|
||
|
topic2id[topic].id = obj._id;
|
||
|
id2topic[obj._id] = topic;
|
||
|
|
||
|
if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message);
|
||
|
|
||
|
if (typeof topic2id[topic].message === 'object') {
|
||
|
adapter.setForeignState(topic2id[topic].id, topic2id[topic].message);
|
||
|
} else {
|
||
|
adapter.setForeignState(topic2id[topic].id, {val: topic2id[topic].message, ack: topic2id[topic].isAck});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
} else if (topic2id[topic].id === null) {
|
||
|
if (config.debug) adapter.log.debug('Client received (but in process) "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message);
|
||
|
topic2id[topic].message = message;
|
||
|
} else {
|
||
|
if (!config.onchange) {
|
||
|
if (topic2id[topic].message !== undefined) delete topic2id[topic].message;
|
||
|
if (topic2id[topic].isAck !== undefined) delete topic2id[topic].isAck;
|
||
|
}
|
||
|
if (typeof message === 'object') {
|
||
|
if (!config.onchange || JSON.stringify(topic2id[topic].message) !== JSON.stringify(message)) {
|
||
|
if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof message + '): ' + message);
|
||
|
adapter.setForeignState(topic2id[topic].id, message);
|
||
|
} else {
|
||
|
if (config.debug) adapter.log.debug('Client received (but ignored) "' + topic + '" (' + typeof message + '): ' + message);
|
||
|
}
|
||
|
} else {
|
||
|
if (!config.onchange || topic2id[topic].message !== message || topic2id[topic].isAck !== isAck) {
|
||
|
if (config.onchange) {
|
||
|
topic2id[topic].message = message;
|
||
|
topic2id[topic].isAck = isAck;
|
||
|
}
|
||
|
if (config.debug) adapter.log.debug('Client received "' + topic + '" (' + typeof message + '): ' + message);
|
||
|
adapter.setForeignState(topic2id[topic].id, {val: message, ack: isAck});
|
||
|
} else {
|
||
|
if (config.debug) adapter.log.debug('Client received (but ignored) "' + topic + '" (' + typeof topic2id[topic].message + '): ' + topic2id[topic].message);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
client.on('connect', () => {
|
||
|
adapter.log.info('Connected to ' + config.url);
|
||
|
connected = true;
|
||
|
adapter.setState('info.connection', connected, true);
|
||
|
|
||
|
for (let i = 0; i < config.patterns.length; i++) {
|
||
|
config.patterns[i] = config.patterns[i].trim();
|
||
|
adapter.log.info('Subscribe on: "' + config.patterns[i] + '"');
|
||
|
client.subscribe(config.patterns[i]);
|
||
|
}
|
||
|
|
||
|
if (config.publishAllOnStart) {
|
||
|
const toPublish = [];
|
||
|
for (const id in states) {
|
||
|
if (states.hasOwnProperty(id) && !messageboxRegex.test(id)) {
|
||
|
toPublish.push(id);
|
||
|
}
|
||
|
}
|
||
|
publishAllStates(config, toPublish);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
client.on('error', err => {
|
||
|
adapter.log.error('Client error:' + err);
|
||
|
|
||
|
if (connected) {
|
||
|
adapter.log.info('Disconnected from ' + config.url);
|
||
|
connected = false;
|
||
|
adapter.setState('info.connection', connected, true);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
client.on('close', err => {
|
||
|
if (connected) {
|
||
|
adapter.log.info('Disconnected from ' + config.url + ': ' + err);
|
||
|
connected = false;
|
||
|
adapter.setState('info.connection', connected, true);
|
||
|
}
|
||
|
});
|
||
|
})(adapter.config);
|
||
|
|
||
|
process.on('uncaughtException', err => adapter.log.error('uncaughtException: ' + err));
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
module.exports = MQTTClient;
|