UNPKG

domotz-remote-pawn

Version:

Domotz Agent

351 lines (299 loc) 12.7 kB
/** 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;