UNPKG

hyperbutter-rainforesteagle

Version:

A plugin to read data from a Rainforest Eagle device into the Hyper Butter server

276 lines (222 loc) 8.16 kB
'use strict'; const EventEmitter = require('events'); const util = require('util'); const net = require('net'); const mdns = require('mdns'); const xml2js = require('xml2js'); const DEFAULTS = { port: 5002, refreshSecs: 4, logSecs: 15, }; const xmlParser = new xml2js.Parser({ explicitArray: false }); /* Inits the module (all config params are optional because of mdns!) @param config.host {String} (Optional) - If you don't want to use auto-discovery, pass in the ip address of your device @param config.port {Number} (Optional) - If your device is running on a different port than 5002, set that here @param config.refreshSecs {Number} (Optional) - How often to get new data (Default is 4) @param config.logSecs {Number} (Optional) - How often to write the current data to the logs (Default is 15) @param config.skipLogging {Boolean} (Optional) - If true, it won't log to the hb logs directory */ function RainforestEagle(config, settings) { EventEmitter.call(this); const deviceData = {}; let socket; let macAddress; let socketData = ''; let loadingData = false; let updateTimer; let logTimer; let retryTimer; // setup listeners this.init = () => { this.emit('subscribe', { 'get-update': this.update, }); // try and connect connect(); }; // set the options config.port = config.port || DEFAULTS.port; config.refreshSecs = config.refreshSecs || DEFAULTS.refreshSecs; config.logSecs = config.logSecs || DEFAULTS.logSecs; const connect = () => { if (config.host === undefined) { // try to find the Eagle on the network findDevice(); } else { tryConnect(); } }; this.update = (callback) => { clearTimeout(retryTimer); if (macAddress === undefined || loadingData) { retryTimer = setTimeout(this.update, 300, callback); return; } // sometimes the socket will timeout, so we setup a longer retry timer to attempt to jump start it retryTimer = setTimeout(this.update, config.refreshSecs * 1000 * 3, callback); const updateSocket = net.createConnection(config.port, config.host); let updateSocketData = ''; updateSocket.on('connect', () => { updateSocket.write(`<LocalCommand>\n <Name>get_instantaneous_demand</Name>\n <MacId>${macAddress}</MacId>\n</LocalCommand>\n`); }); updateSocket.on('data', data => updateSocketData += data.toString()); updateSocket.on('error', (error) => { if (callback) callback(error); this.emit('warn', `Update error: ${error}`); }); updateSocket.on('end', () => { parseSocketData((error, result) => { if (result.InstantaneousDemand !== undefined) { // send the device data up deviceData.time = new Date().getTime(); deviceData.watts = hexToInt(result.InstantaneousDemand.Demand.substr(2)); if (callback) callback(null, deviceData); this.emit('update', deviceData); } else { // something funky happened on the device this.emit('warn', 'Can\'t seem to read the watts, maybe something is up with your device'); } // go get it again setTimeout(this.update, config.refreshSecs * 1000, callback); clearTimeout(retryTimer); updateSocketData = ''; }, updateSocketData); }); }; const findDevice = () => { this.emit('debug', 'Searching for "eagle-", "workstation" on port 9'); // this fixes a bug with mdns const sequence = [ mdns.rst.DNSServiceResolve(), 'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({ families:[0] }), mdns.rst.makeAddressesUnique(), ]; const browser = mdns.createBrowser(mdns.tcp('workstation'), { resolverSequence: sequence }); // go find it browser.on('serviceUp', (device) => { if (device.port === 9 && device.name.includes('eagle-')) { // found it, probably! this.emit('debug', `Found device @ ${device.host}`); config.host = device.host; tryConnect(); // stop looking browser.stop(); } }); browser.start(); }; const tryConnect = () => { this.emit('status', `Connecting to ${config.host}:${config.port}`); socket = net.createConnection(config.port, config.host); socket.on('connect', function(){ this.emit('debug', 'Getting MAC address'); socket.write('<LocalCommand>\n <Name>list_devices</Name>\n</LocalCommand>\n'); }); socket.on('data', onSocketData); socket.on('error', (error) => { this.emit('error', `Connection error: ${error}`); }); socket.on('end', () => { parseSocketData((error, result) => { if (error) this.emit('error', error); else { macAddress = result.DeviceInfo.DeviceMacId.toString(); this.emit('debug', `MAC address: ${macAddress}`); getDeviceData(); } }); }); }; // append the data const onSocketData = data => socketData += data.toString(); const parseSocketData = (callback, data) => { if (data === undefined) data = socketData; xmlParser.parseString(`<rootNode>\n ${data.toString()}</rootNode>`, (error, result) => { if (result) result = result.rootNode; if (callback) callback(error, result); }); }; const getDeviceData = () => { if (macAddress === undefined) return; if (deviceData.watts !== undefined) { // send it off this.emit('update', deviceData); return; } if (loadingData) { this.emit('debug', 'Eagle still loading data'); return; } loadingData = true; if (socket !== undefined) { socket.end(); socket = undefined; } socket = net.createConnection(config.port, config.host); socket.on('connect', () => { this.emit('debug', 'Getting device data'); socket.write(`<LocalCommand>\n <Name>get_device_data</Name>\n <MacId>${macAddress}</MacId>\n</LocalCommand>\n`); }); socket.on('data', onSocketData); socket.on('error', (error) => { // the connection closed for some reason, let's just reconnect this.emit('warn', `Socket closed, re-connecting: ${error}`); connect(); }); socket.on('end', () => { this.emit('status', 'Connected'); this.emit('connected'); parseSocketData((error, result) => { // send the device data up deviceData.time = new Date().getTime(); deviceData.watts = hexToInt(result.InstantaneousDemand.Demand.substr(2)); this.emit('update', deviceData); loadingData = false; socketData = ''; // schedule an update if (updateTimer === undefined) { updateTimer = setTimeout(this.update, config.refreshSecs * 1000); } // setup the logger if(logTimer === undefined && config.skipLogging !== true) { logTimer = setInterval(logData, config.logSecs * 1000); } }); }); }; const logData = () => { // nothing to log yet if (deviceData.watts === undefined) return; const date = new Date(); let history; let month = date.getMonth() + 1; if (month < 10) month = `0${month}`; const filename = `${date.getFullYear()}-${month}.log`; this.emit('read-log', filename, (error, data) => { if (error) { // we need to create the history history = [deviceData]; } else { // parse the data and then write the log file history = JSON.parse(data); history.push(deviceData); } this.emit('write-log', filename, JSON.stringify(history), (error) => { if (error) this.emit('warn', `Not able to write log to: ${filename}`); }); }); } // http://stackoverflow.com/a/34679269 const hexToInt = (hex) => { if (hex.length % 2 !== 0) hex = `0${hex}`; const maxVal = Math.pow(2, hex.length / 2 * 8); let num = parseInt(hex, 16); if (num > maxVal / 2 - 1) num = num - maxVal; return num; }; return this; } util.inherits(RainforestEagle, EventEmitter); module.exports = RainforestEagle;