Initial commit
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user