domotz-remote-pawn
Version:
Domotz Agent
351 lines (299 loc) • 12.7 kB
JavaScript
/** This file is part of Domotz Agent.
* Copyright (C) 2016 Domotz Ltd
*
* Domotz Agent is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Domotz Agent is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Domotz Agent. If not, see <http://www.gnu.org/licenses/>.
**/
/**
* Created by Andrea Azzarà <a.azzara@domotz.com> on 4/07/16.
*
*/
const MODULE = 'CONTROL4 ';
const SSH_EXEC_TIMEOUT = 10000;
const MAX_PARALLEL_SSH_ZMAN = 4;
// #EOT must not be enclosed in quotes to use variable inside the heredoc\n
const SHOW_NODE_IN = 'zman <<EOT\nshownode %s\nexit\nEOT';
const SHOW_NODES_IN = 'zman <<\'EOT\'\nshownodes\nexit\nEOT\ndate \'+%Y-%m-%d %H:%M:%S %z\'';
const SHOW_ZAPS_IN = 'zman <<\'EOT\'\nshowzaps\nexit\nEOT';
const SHOW_LINKS_IN = 'zman <<\'EOT\'\nshowlinks\nexit\nEOT';
var factory = function (async, q, ssh, _, util) {
function getOptsForSshCommand(password, user, host) {
return {
user: user,
password: password,
host: host,
host_key_checking: 'no',
kex_algorithm: 'diffie-hellman-group1-sha1',
disable_pseudo_tty_alloc: true,
log_prefix: MODULE,
ttl: SSH_EXEC_TIMEOUT
};
}
function convertToDate(weekday, day, month, time, timezone, utcOffset, year) {
console.silly('WeekDay: %s, Day: %s, Month: %s, Year: %s, Time: %s, ' +
'TimeZone: %s, UTCOffset: %s', weekday, day, month, year, time,
timezone, utcOffset);
year = typeof year === 'undefined' ? new Date().getFullYear() : year;
utcOffset = typeof utcOffset === 'undefined' ? 0 : utcOffset;
timezone = typeof timezone === 'undefined' ? 'GMT' : timezone;
var dateString = weekday + ', ' + day + ' ' + month + ' ' + year + ' ' + time + ' ' + timezone;
return new Date(Date.parse(dateString) + parseInt(utcOffset)).toISOString();
}
function splitOutput(output) {
return output.toString().split(/\r?\n/);
}
function isZigbeeLine(line) {
return line.indexOf('0x') === 0;
}
function subString(string, start, len) {
return string.substring(start, start + len).trim();
}
function splitAndFilter(line) {
const SEP = ' ';
return line.split(SEP).filter(function (value) {
return value !== '';
});
}
function parseShowNodes(output) {
var lines = splitOutput(output);
var nodes = [];
var controllerDate = lines.pop(); // The last line is the date
var controllerUTCTime = new Date(controllerDate).getTime();
var controllerUTCOffset = Date.now() - controllerUTCTime;
var controllerTZOffset = controllerDate.split(' ')[2];
console.silly('Controller Info: [Date => %s, UTC Time => %s, ' +
'UTC Offset => %s, TimezoneOffset => %s', controllerDate,
controllerUTCTime, controllerUTCTime, controllerUTCOffset);
var getNodeType = function (type) {
if (type.indexOf('Sleepy') >= 0) {
return 'sleepy end device';
} else if (type.indexOf('Router') >= 0) {
return 'router';
} else {
return 'end device';
}
};
var POS = {
SHOW_NODES: {
LID: {START: 0, LEN: 20},
SID: {START: 20, LEN: 10},
STATUS: {START: 30, LEN: 10},
FIRMWARE: {START: 40, LEN: 14},
MODEL: {START: 54, LEN: 31},
TIME: {START: 85, LEN: 20},
TYPE: {START: 111, LEN: 16}
}
};
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (isZigbeeLine(line)) {
var splittedLine = splitAndFilter(line);
var dateString = splitAndFilter(subString(line, POS.SHOW_NODES.TIME.START, POS.SHOW_NODES.TIME.LEN));
var weekDay = dateString[0];
var month = dateString[1];
var day = dateString[2];
var time = dateString[3];
var lastSeen;
try {
lastSeen = convertToDate(weekDay, day, month, time, controllerTZOffset, controllerUTCOffset);
} catch (error) {
console.error("unable to convert last contact to date: " + JSON.stringify(error));
lastSeen = null;
}
var node_type = getNodeType(subString(line, POS.SHOW_NODES.TYPE.START, POS.SHOW_NODES.TYPE.LEN));
var best_zap = splittedLine[splittedLine.length - 1];
var node = {
lid: splittedLine[0],
sid: splittedLine[1],
status: subString(line, POS.SHOW_NODES.STATUS.START, POS.SHOW_NODES.STATUS.LEN).toUpperCase(),
product_name: subString(line, POS.SHOW_NODES.MODEL.START, POS.SHOW_NODES.MODEL.LEN),
last_seen: lastSeen,
node_type: node_type,
best_zap: best_zap,
details: {firmware: subString(line, POS.SHOW_NODES.FIRMWARE.START, POS.SHOW_NODES.FIRMWARE.LEN)}
};
nodes.push(node);
console.silly(MODULE + "Parsed show node output: " + JSON.stringify(node));
}
}
return nodes;
}
function parseShowZaps(output) {
var lines = splitOutput(output);
var zaps = [];
for (var i = 0; i < lines.length; i++) {
if (isZigbeeLine(lines[i])) {
var splittedLine = splitAndFilter(lines[i]);
var zap = {
lid: splittedLine[0],
sid: splittedLine[1],
ip: splittedLine[2],
port: splittedLine[3],
status: splittedLine[4],
coordinator: splittedLine[5],
channel: splittedLine[16]
};
zaps.push(zap);
}
}
console.silly(MODULE + "Parsed show zaps output: " + JSON.stringify(zaps));
return zaps;
}
function omitNa(elem) {
const notAvailable = 'na';
return elem === notAvailable;
}
function parseShowLinks(output) {
var lines = splitOutput(output);
var links = [];
for (var i = 0; i < lines.length; i++) {
if (isZigbeeLine(lines[i])) {
var splittedLine = splitAndFilter(lines[i]);
var link = {
lid: splittedLine[0],
sid: splittedLine[1],
link: splittedLine[2],
rx_lqi: splittedLine[3],
tx_lqi: splittedLine[5],
rssi: splittedLine[4]
};
var cleanLink = _.omit(link, omitNa);
links.push(cleanLink);
}
}
console.silly(MODULE + "Parsed show zaps output: " + JSON.stringify(links));
return links.filter(function (elem) { //filter objects without relevant data
return elem.rx_lqi || elem.tx_lqi || elem.rssi || elem.link;
});
}
function parseShowNode(output) {
var lines = splitOutput(output);
var node_details = {routes: {}};
lines.forEach(function (line) {
var splittedLine = line.split(/\.*:\s*/);
if (splittedLine.length === 2) {
var key = splittedLine[0].trim().replace(/ /g, '_').toLowerCase();
var value = splittedLine[1];
if (value.length > 0) {
node_details[key] = value;
}
}
else if (splittedLine.length === 3) {
var dest = splittedLine[1].split(' ')[0];
node_details.routes[dest] = splittedLine[2];
}
else if (line.indexOf('MAC Tx Unicasts') >= 0) {
node_details.mac_tx_unicasts = line.split('MAC Tx Unicasts')[1].trim();
}
else if (line.indexOf('Reflash Time:') >= 0) {
node_details.reflash_time = line.split('Reflash Time:')[1].trim();
}
});
var node = {
battery_level: node_details.battery_level,
sid: node_details.sid,
uptime: node_details.uptime
};
var detailsRedundantFields = [
'battery_level',
'signal_strength',
'channel',
'status',
'node_type',
'lid',
'sid',
'rx_lqi_(nb_to_node)',
'rssi_(nb_to_node)',
'tx_lqi_(nb_to_node)'];
node_details = _.omit(node_details, omitNa);
node_details = _.omit(node_details, detailsRedundantFields); //info already available from fast discovery
node.details = node_details;
node = _.omit(node, omitNa);
console.silly(MODULE + "Parsed show node output: " + JSON.stringify(node));
return node;
}
function fastDiscovery(host, user, password) {
console.info(MODULE + "Control4 Fast discovery");
var nodes, zaps, links;
return ssh.exec(SHOW_NODES_IN, getOptsForSshCommand(password, user, host))
.then(function (stdout) {
console.info(MODULE + "Executed show nodes command");
nodes = parseShowNodes(stdout);
return ssh.exec(SHOW_ZAPS_IN, getOptsForSshCommand(password, user, host));
})
.then(function (stdout) {
console.info(MODULE + "Executed show zaps command");
zaps = parseShowZaps(stdout);
return ssh.exec(SHOW_LINKS_IN, getOptsForSshCommand(password, user, host));
})
.then(function (stdout) {
console.info(MODULE + "Executed show links command");
links = parseShowLinks(stdout);
var message = {
nodes: nodes,
zaps: zaps,
links: links
};
console.silly(MODULE + "Returning fast discovery results to the worker: " + JSON.stringify(message, null, 3));
return message;
});
}
function fullDiscovery(host, user, password, nodesList) {
var deferred = q.defer();
console.info(MODULE + "Control4 Full discovery");
if (nodesList.length === 0) {
deferred.resolve({});
return deferred.promise;
}
var sshOptions = getOptsForSshCommand(password, user, host);
function nodeDiscovery(sid, cb) {
ssh.exec(util.format(SHOW_NODE_IN, sid), sshOptions).then(function (stdout) {
var node = parseShowNode(stdout);
cb(null, node);
}).catch(function (error) {
console.error("Node discovery error" + JSON.stringify(error));
cb(null, null); //return a null value in the final array in case of error
});
}
var functions = [];
for (var i = 0; i < nodesList.length; i++) {
functions.push(nodeDiscovery.bind(null, nodesList[i]));
}
async.parallelLimit(functions, MAX_PARALLEL_SSH_ZMAN, function (err, details) {
if (err) {
deferred.reject(err);
return;
}
var nodesDict = {};
details.filter(function (elem) {
return elem !== null;
}).forEach(function (node) {
nodesDict[node.sid] = node;
});
console.silly(MODULE + "Returning full discovery results to the worker: " + JSON.stringify(nodesDict, null, 3));
deferred.resolve(nodesDict);
});
return deferred.promise;
}
return {
fastDiscovery: fastDiscovery,
fullDiscovery: fullDiscovery,
/* test code */
parseShowLinks: parseShowLinks,
parseShowZaps: parseShowZaps,
parseShowNodes: parseShowNodes,
convertToDate: convertToDate
/* test code */
};
};
module.exports.factory = factory;