You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
14 KiB
375 lines
14 KiB
/* jshint -W097 */// jshint strict:false
|
|
/*jslint node: true */
|
|
'use strict';
|
|
var utils = require(__dirname + '/lib/utils'); // Get common adapter utils
|
|
var adapter = utils.Adapter('owntracks');
|
|
//var LE = require(utils.controllerDir + '/lib/letsencrypt.js');
|
|
var createStreamServer = require('create-stream-server');
|
|
var mqtt = require('mqtt-connection');
|
|
|
|
var server;
|
|
var clients = {};
|
|
var objects = {};
|
|
|
|
function decrypt(key, value) {
|
|
var result = '';
|
|
for (var i = 0; i < value.length; ++i) {
|
|
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// is called when adapter shuts down - callback has to be called under any circumstances!
|
|
adapter.on('unload', function (callback) {
|
|
try {
|
|
adapter.log.info('cleaned everything up...');
|
|
if (server) {
|
|
server.destroy();
|
|
server = null;
|
|
}
|
|
callback();
|
|
} catch (e) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
adapter.on('ready', main);
|
|
|
|
function createUser(user) {
|
|
var id = adapter.namespace + '.users.' + user.replace(/\s|\./g, '_');
|
|
adapter.getForeignObject(id + '.battery', function (err, obj) {
|
|
if (!obj) {
|
|
adapter.setForeignObject(id + '.battery', {
|
|
common: {
|
|
name: 'Device battery level for ' + user,
|
|
min: 0,
|
|
max: 100,
|
|
unit: '%',
|
|
role: 'battery',
|
|
type: 'number'
|
|
},
|
|
type: 'state',
|
|
native: {}
|
|
});
|
|
}
|
|
});
|
|
adapter.getForeignObject(id + '.latitude', function (err, obj) {
|
|
if (!obj) {
|
|
adapter.setForeignObject(id + '.latitude', {
|
|
common: {
|
|
name: 'Latitude for ' + user,
|
|
role: 'gps.latitude',
|
|
type: 'number'
|
|
},
|
|
type: 'state',
|
|
native: {}
|
|
});
|
|
}
|
|
});
|
|
adapter.getForeignObject(id + '.longitude', function (err, obj) {
|
|
if (!obj) {
|
|
adapter.setForeignObject(id + '.longitude', {
|
|
common: {
|
|
name: 'Longitude for ' + user,
|
|
role: 'gps.longitude',
|
|
type: 'number'
|
|
},
|
|
type: 'state',
|
|
native: {}
|
|
});
|
|
}
|
|
});
|
|
adapter.getForeignObject(id + '.accuracy', function (err, obj) {
|
|
if (!obj) {
|
|
adapter.setForeignObject(id + '.accuracy', {
|
|
common: {
|
|
name: 'Accuracy for ' + user,
|
|
role: 'state',
|
|
unit: 'm',
|
|
type: 'number'
|
|
},
|
|
type: 'state',
|
|
native: {}
|
|
});
|
|
}
|
|
});
|
|
adapter.getForeignObject(id + '.timestamp', function (err, obj) {
|
|
if (!obj) {
|
|
adapter.setForeignObject(id + '.timestamp', {
|
|
common: {
|
|
name: 'Timestamp for ' + user,
|
|
role: 'state',
|
|
type: 'number'
|
|
},
|
|
type: 'state',
|
|
native: {}
|
|
});
|
|
}
|
|
});
|
|
adapter.getForeignObject(id + '.datetime', function (err, obj) {
|
|
if (!obj) {
|
|
adapter.setForeignObject(id + '.datetime', {
|
|
common: {
|
|
name: 'Datetime for ' + user,
|
|
role: 'state',
|
|
type: 'string'
|
|
},
|
|
type: 'state',
|
|
native: {}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function sendState2Client(client, topic, payload) {
|
|
// client has subscription for this ID
|
|
if (client._subsID && client._subsID[topic]) {
|
|
client.publish({topic: topic, payload: payload});
|
|
} else
|
|
// Check patterns
|
|
if (client._subs) {
|
|
for (var s in client._subs) {
|
|
if (!client._subs.hasOwnProperty(s)) continue;
|
|
if (client._subs[s].regex.exec(topic)) {
|
|
client.publish({topic: topic, payload: payload});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function processTopic(topic, payload, ignoreClient) {
|
|
for (var k in clients) {
|
|
// if get and set have different topic names, send state to issuing client too.
|
|
if (clients[k] === ignoreClient) continue;
|
|
sendState2Client(clients[k], topic, payload);
|
|
}
|
|
}
|
|
|
|
var cltFunction = function (client) {
|
|
client.on('connect', function (packet) {
|
|
client.id = packet.clientId;
|
|
if (adapter.config.user) {
|
|
if (adapter.config.user != packet.username ||
|
|
adapter.config.pass != packet.password) {
|
|
adapter.log.warn('Client [' + packet.clientId + '] has invalid password(' + packet.password + ') or username(' + packet.username + ')');
|
|
client.connack({returnCode: 4});
|
|
if (clients[client.id]) delete clients[client.id];
|
|
client.stream.end();
|
|
return;
|
|
}
|
|
}
|
|
|
|
adapter.log.info('Client [' + packet.clientId + '] connected');
|
|
client.connack({returnCode: 0});
|
|
clients[client.id] = client;
|
|
});
|
|
|
|
client.on('publish', function (packet) {
|
|
var isAck = true;
|
|
var topic = packet.topic;
|
|
var message = packet.payload;
|
|
adapter.log.debug('publish "' + topic + '": ' + message);
|
|
|
|
if (packet.qos == 1) {
|
|
client.puback({ messageId: packet.messageId});
|
|
}
|
|
else if (packet.qos == 2) {
|
|
client.pubrec({ messageId: packet.messageId});
|
|
}
|
|
// "owntracks/yunkong2/klte":
|
|
// {
|
|
// "_type":"location", // location, lwt, transition, configuration, beacon, cmd, steps, card, waypoint
|
|
// "acc":50, // accuracy of location in meters
|
|
// "batt":46, // is the device's battery level in percent (integer)
|
|
// "lat":49.0026446, // latitude
|
|
// "lon":8.3832128, // longitude
|
|
// "tid":"te", // is a configurable tracker-ID - ignored
|
|
// "tst":1472987109 // UNIX timestamp in seconds
|
|
// }
|
|
var parts = topic.split('/');
|
|
if (parts[1] !== adapter.config.user) {
|
|
adapter.log.warn('publish "' + topic + '": invalid user name - "' + parts[1] + '"');
|
|
return;
|
|
}
|
|
if (!objects[parts[2]]) {
|
|
// create object
|
|
createUser(parts[2]);
|
|
objects[parts[2]] = true;
|
|
}
|
|
processTopic(topic, message);
|
|
try {
|
|
var obj = JSON.parse(message);
|
|
if (obj._type === 'location') {
|
|
if (obj.acc !== undefined) {
|
|
adapter.setState('users.' + parts[2] + '.accuracy', {val: obj.acc, ts: obj.tst * 1000, ack: true});
|
|
}
|
|
if (obj.batt !== undefined) {
|
|
adapter.setState('users.' + parts[2] + '.battery', {val: obj.batt, ts: obj.tst * 1000, ack: true});
|
|
}
|
|
if (obj.lon !== undefined) {
|
|
adapter.setState('users.' + parts[2] + '.longitude', {val: obj.lon, ts: obj.tst * 1000, ack: true});
|
|
}
|
|
if (obj.lat !== undefined) {
|
|
adapter.setState('users.' + parts[2] + '.latitude', {val: obj.lat, ts: obj.tst * 1000, ack: true});
|
|
}
|
|
if (obj.tst !== undefined) {
|
|
adapter.setState('users.' + parts[2] + '.timestamp', {val: obj.tst, ts: obj.tst * 1000, ack: true});
|
|
var date = new Date(obj.tst * 1000);
|
|
var day = '0' + date.getDate();
|
|
var month = '0' + (date.getMonth() + 1);
|
|
var year = date.getFullYear();
|
|
var hours = '0' + date.getHours();
|
|
var minutes = '0' + date.getMinutes();
|
|
var seconds = '0' + date.getSeconds();
|
|
var formattedTime = day.substr(-2) + '.' + month.substr(-2) + '.' + year + ' ' + hours.substr(-2) + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
|
|
|
|
adapter.setState('users.' + parts[2] + '.datetime', {val: formattedTime, ts: obj.tst * 1000, ack: true});
|
|
}
|
|
|
|
}
|
|
} catch (e) {
|
|
adapter.log.error('Cannot parse payload: ' + message);
|
|
}
|
|
});
|
|
|
|
client.on('subscribe', function (packet) {
|
|
var granted = [];
|
|
if (!client._subsID) client._subsID = {};
|
|
if (!client._subs) client._subs = {};
|
|
|
|
for (var i = 0; i < packet.subscriptions.length; i++) {
|
|
granted.push(packet.subscriptions[i].qos);
|
|
|
|
var topic = packet.subscriptions[i].topic;
|
|
|
|
adapter.log.debug('Subscribe on ' + topic);
|
|
// if pattern without wildchars
|
|
if (topic.indexOf('*') === -1 && topic.indexOf('#') === -1 && topic.indexOf('+') === -1) {
|
|
client._subsID[topic] = {
|
|
qos: packet.subscriptions[i].qos
|
|
};
|
|
} else {
|
|
// "owntracks/+/+/info" => owntracks\/.+\/.+\/info
|
|
var pattern = topic.replace(/\//g, '\\/').replace(/\+/g, '[^\\/]+').replace(/\*/g, '.*');
|
|
pattern = '^' + pattern + '$';
|
|
|
|
// add simple pattern
|
|
client._subs[topic] = {
|
|
regex: new RegExp(pattern),
|
|
qos: packet.subscriptions[i].qos,
|
|
pattern: topic
|
|
};
|
|
}
|
|
}
|
|
|
|
client.suback({granted: granted, messageId: packet.messageId});
|
|
//Subscribe on owntracks/+/+
|
|
//Subscribe on owntracks/+/+/info
|
|
//Subscribe on owntracks/yunkong2/denis/cmd
|
|
//Subscribe on owntracks/+/+/event
|
|
//Subscribe on owntracks/+/+/waypoint
|
|
|
|
// send to client all images
|
|
if (adapter.config.pictures && adapter.config.pictures.length) {
|
|
setTimeout(function () {
|
|
for (var p = 0; p < adapter.config.pictures.length; p++) {
|
|
var text = adapter.config.pictures[p].base64.split(',')[1]; // string has form data:;base64,TEXT==
|
|
sendState2Client(client, 'owntracks/' + adapter.config.user + '/' + adapter.config.pictures[p].name + '/info',
|
|
JSON.stringify({
|
|
_type: 'card',
|
|
name: adapter.config.pictures[p].name,
|
|
face: text
|
|
})
|
|
);
|
|
}
|
|
}, 200);
|
|
}
|
|
});
|
|
|
|
client.on('pingreq', function (packet) {
|
|
adapter.log.debug('Client [' + client.id + '] pingreq');
|
|
client.pingresp();
|
|
});
|
|
|
|
client.on('disconnect', function (packet) {
|
|
adapter.log.info('Client [' + client.id + '] disconnected');
|
|
client.stream.end();
|
|
});
|
|
|
|
client.on('close', function (err) {
|
|
adapter.log.info('Client [' + client.id + '] closed');
|
|
delete clients[client.id];
|
|
});
|
|
|
|
client.on('error', function (err) {
|
|
adapter.log.warn('[' + client.id + '] ' + err);
|
|
});
|
|
};
|
|
|
|
|
|
function initMqttServer(config) {
|
|
var serverConfig = {};
|
|
var options = {
|
|
ssl: config.certificates,
|
|
emitEvents: true // default
|
|
};
|
|
|
|
config.port = parseInt(config.port, 10) || 1883;
|
|
|
|
if (config.ssl) {
|
|
serverConfig.mqtts = 'ssl://0.0.0.0:' + config.port;
|
|
if (config.webSocket) {
|
|
serverConfig.mqtwss = 'wss://0.0.0.0:' + (config.port + 1);
|
|
}
|
|
} else {
|
|
serverConfig.mqtts = 'tcp://0.0.0.0:' + config.port;
|
|
if (config.webSocket) {
|
|
serverConfig.mqtwss = 'ws://0.0.0.0:' + (config.port + 1);
|
|
}
|
|
}
|
|
|
|
server = createStreamServer(serverConfig, options, function (clientStream) {
|
|
cltFunction(mqtt(clientStream, {
|
|
notData: !options.emitEvents
|
|
}));
|
|
});
|
|
|
|
// to start
|
|
server.listen(function () {
|
|
if (config.ssl) {
|
|
adapter.log.info('Starting MQTT (Secure) ' + (config.user ? 'authenticated ' : '') + 'server on port ' + config.port);
|
|
if (config.webSocket) {
|
|
adapter.log.info('Starting MQTT-WebSocket (Secure) ' + (config.user ? 'authenticated ' : '') + 'server on port ' + (config.port + 1));
|
|
}
|
|
} else {
|
|
adapter.log.info('Starting MQTT ' + (config.user ? 'authenticated ' : '') + 'server on port ' + config.port);
|
|
if (config.webSocket) {
|
|
adapter.log.info('Starting MQTT-WebSocket ' + (config.user ? 'authenticated ' : '') + 'server on port ' + (config.port + 1));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
//noinspection JSUnresolvedVariable
|
|
adapter.config.pass = decrypt('Zgfr56gFe87jJOM', adapter.config.pass);
|
|
|
|
if (!adapter.config.user) {
|
|
adapter.log.error('Empty user name not allowed!');
|
|
process.stop(-1);
|
|
return;
|
|
}
|
|
|
|
if (adapter.config.secure) {
|
|
// Load certificates
|
|
adapter.getCertificates(function (err, certificates, leConfig) {
|
|
adapter.config.certificates = certificates;
|
|
adapter.config.leConfig = leConfig;
|
|
initMqttServer(adapter.config);
|
|
});
|
|
} else {
|
|
initMqttServer(adapter.config);
|
|
}
|
|
}
|