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
JavaScript
;
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;