Skip to content

Commit

Permalink
8.7 release
Browse files Browse the repository at this point in the history
- Voltage logging/graphing by default
- logging is done only if data value is different or enough time has
passed (1h)
- fixed bug with graph showing on non graphed metrics
- added ability to override graph Yaxis min/max/autoscaleMargin in
metrics definitions
- added sample motion/temperature SMS events with limiter (SMS only once
per N unit of time)
- other minor adjustments
  • Loading branch information
LowPowerLab committed Oct 18, 2016
1 parent e3d1813 commit b3e2f21
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 28 deletions.
10 changes: 5 additions & 5 deletions gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ io.sockets.on('connection', function (socket) {
db.findOne({_id:node.nodeId}, function (err, doc) {
if (doc == null)
{
var entry = { _id:node.nodeId, updated:(new Date).getTime(), label:node.label || 'NEW NODE', metrics:{} };
var entry = { _id:node.nodeId, updated:Date.now(), label:node.label || 'NEW NODE', metrics:{} };
db.insert(entry);
console.log(' ['+node.nodeId+'] DB-Insert new _id:' + node.nodeId);
socket.emit('LOG', 'NODE INJECTED, ID: ' + node.nodeId);
Expand Down Expand Up @@ -440,15 +440,15 @@ global.processSerialData = function (data) {
}

//check for duplicate messages - this can happen when the remote node sends an ACK-ed message but does not get the ACK so it resends same message repeatedly until it receives an ACK
if (existingNode.updated != undefined && ((new Date) - new Date(existingNode.updated).getTime()) < 500 && msgHistory[id] == msgTokens)
if (existingNode.updated != undefined && (Date.now() - existingNode.updated < 500) && msgHistory[id] == msgTokens)
{ console.log(" DUPLICATE, skipping..."); return; }

msgHistory[id] = msgTokens;

//console.log('FOUND ENTRY TO UPDATE: ' + JSON.stringify(existingNode));
existingNode._id = id;
existingNode.rssi = rssi; //update signal strength we last heard from this node, regardless of any matches
existingNode.updated = new Date().getTime(); //update timestamp we last heard from this node, regardless of any matches
existingNode.updated = Date.now(); //update timestamp we last heard from this node, regardless of any matches
if (existingNode.metrics == undefined)
existingNode.metrics = new Object();
if (existingNode.events == undefined)
Expand Down Expand Up @@ -508,7 +508,7 @@ global.processSerialData = function (data) {
}

//prepare entry to save to DB, undefined values will not be saved, hence saving space
var entry = {_id:id, updated:existingNode.updated, type:existingNode.type||undefined, label:existingNode.label||undefined, descr:existingNode.descr||undefined, hidden:existingNode.hidden||undefined, /*V:existingNode.V||undefined,*/ rssi:existingNode.rssi, metrics:Object.keys(existingNode.metrics).length > 0 ? existingNode.metrics : {}, events: Object.keys(existingNode.events).length > 0 ? existingNode.events : undefined };
var entry = {_id:id, updated:existingNode.updated, type:existingNode.type||undefined, label:existingNode.label||undefined, descr:existingNode.descr||undefined, hidden:existingNode.hidden||undefined, rssi:existingNode.rssi, metrics:Object.keys(existingNode.metrics).length > 0 ? existingNode.metrics : {}, events: Object.keys(existingNode.events).length > 0 ? existingNode.events : undefined };
//console.log('UPDATING ENTRY: ' + JSON.stringify(entry));

//save to DB
Expand Down Expand Up @@ -552,7 +552,7 @@ function schedule(node, eventKey) {
var nextRunTimeout = metricsDef.events[eventKey].nextSchedule(node);
if (nextRunTimeout < 1000)
{
console.ERROR('**** SCHEDULING EVENT ERROR - nodeId:' + node._id+' event:'+eventKey+' cannot schedule event in ' + nextRunTimeout + 'ms (less than 1s)');
console.error('**** SCHEDULING EVENT ERROR - nodeId:' + node._id+' event:'+eventKey+' cannot schedule event in ' + nextRunTimeout + 'ms (less than 1s)');
return;
}
var hrs = parseInt(nextRunTimeout/3600000);
Expand Down
28 changes: 18 additions & 10 deletions logUtil.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// *******************************************************************************
// This is the logging storage engine for the Moteino Gateway.
// This is the logging storage engine for the Moteino IoT Gateway.
// It is a vast improvement over storing data in memory (previously done in neDB)
// http://lowpowerlab.com/gateway
// Some of this work was inspired by work done by Timestore and OpenEnergyMonitor:
Expand Down Expand Up @@ -80,14 +80,17 @@ exports.getData = function(filename, start, end, dpcount) {
return {data:data, queryTime:(new Date() - ts), totalIntervalDatapoints: (posEnd-posStart)/9+1 };
}

// filename: binary file to append new data point to
// timestamp: data point timestamp (seconds since unix epoch)
// value: data point value (signed integer)
exports.postData = function post(filename, timestamp, value) {
if (!metrics.isNumeric(value)) value = 999; //catch all value
var logsize = exports.fileSize(filename);
if (logsize % 9 > 0) throw 'File ' + filename +' is not multiple of 9bytes, post aborted';

var fd;
var buff = new Buffer(9);
var tmp = 0, pos = 0;
var lastTime = 0, lastValue = 0, pos = 0;
value=Math.round(value*10000); //round to make an exactly even integer

//prepare 9 byte buffer to write
Expand All @@ -99,17 +102,22 @@ exports.postData = function post(filename, timestamp, value) {
if (logsize>=9) {
// read the last value appended to the file
fd = fs.openSync(filename, 'r');
var buf4 = new Buffer(4);
fs.readSync(fd, buf4, 0, 4, logsize-8);
tmp = buf4.readInt32BE(0); //read timestamp (bytes 1-4 bytes in buffer)
var buf8 = new Buffer(8);

fs.readSync(fd, buf8, 0, 8, logsize-8);
lastTime = buf8.readUInt32BE(0); //read timestamp (bytes 0-3 in buffer)
lastValue = buf8.readInt32BE(4); //read value (bytes 4-7 in buffer)
fs.closeSync(fd);

if (timestamp > tmp)
if (timestamp > lastTime)
{
//timestamp is in the future, append
fd = fs.openSync(filename, 'a');
fs.writeSync(fd, buff, 0, 9, logsize);
fs.closeSync(fd);
if (value != lastValue || (timestamp-lastTime>3600)) //only write new value if different than last value or 1 hour has passed (should be a setting?)
{
//timestamp is in the future, append
fd = fs.openSync(filename, 'a');
fs.writeSync(fd, buff, 0, 9, logsize);
fs.closeSync(fd);
}
}
else
{
Expand Down
61 changes: 60 additions & 1 deletion metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
// - value - this can be hardcoded, or if left blank the value will be the first captured parentheses from the regex expression
// - pin:1/0 - if '1' then by default this metric will show up in the main homepage view for that node, otherwise it will only show in the node page; it can then manually be flipped in the UI
// - graph:1/0 - if '1' then by default this metric will be logged in gatewayLog.db every time it comes in
// - if '0' then this would not be logged but can be turned on from the metric details page
// - if not defined then metric is not logged and toggle button is hidden in metric detail page
// - logValue - you can specify a hardcoded value that should be logged instead of the captured metric (has to always be numeric!)
// - graphOptions - this is a javascript object that when presend is injected directly into the FLOT graph for the metric - you can use this to highly customize the appearance of any metric graph
// - it should only be specified one per each metric - the first one (ie one for each set of metrics that have multiple entries with same 'name') - ex: GarageMote 'Status' metric
Expand Down Expand Up @@ -107,7 +109,7 @@ exports.metrics = {
FSTATE : { name:'FSTATE', regexp:/FSTATE\:(AUTO|AUTOCIRC|ON)/i, value:''},

//special metrics
V : { name:'V', regexp:/(?:V?BAT|VOLTS|V)\:(\d+\.\d+)v?/i, value:'', unit:'v'},
V : { name:'V', regexp:/(?:V?BAT|VOLTS|V)\:([\d\.]+)v?/i, value:'', unit:'v', graph:1, graphOptions:{ legendLbl:'Voltage', lines: { fill:false, lineWidth:1 }, grid: { backgroundColor: {colors:['#000', '#03c', '#08c']}}, yaxis: { min: 0, autoscaleMargin: 0.25 }}},
//catchAll : { name:'CatchAll', regexp:/(\w+)\:(\w+)/i, value:''},
};

Expand All @@ -124,6 +126,63 @@ exports.events = {
mailboxAlert : { label:'Mailbox Open Alert!', icon:'audio', descr:'Message sound when mailbox is opened', serverExecute:function(node) { if (node.metrics['M'] && node.metrics['M'].value == 'MOTION' && (Date.now() - new Date(node.metrics['M'].updated).getTime() < 2000)) { io.sockets.emit('PLAYSOUND', 'sounds/incomingmessage.wav'); }; } },
motionEmail : { label:'Motion : Email', icon:'mail', descr:'Send email when MOTION is detected', serverExecute:function(node) { if (node.metrics['M'] && node.metrics['M'].value == 'MOTION' && (Date.now() - new Date(node.metrics['M'].updated).getTime() < 2000)) { sendEmail('MOTION DETECTED', 'MOTION WAS DETECTED ON NODE: [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM'))); }; } },
motionSMS : { label:'Motion : SMS', icon:'comment', descr:'Send SMS when MOTION is detected', serverExecute:function(node) { if (node.metrics['M'] && node.metrics['M'].value == 'MOTION' && (Date.now() - new Date(node.metrics['M'].updated).getTime() < 2000)) { sendSMS('MOTION DETECTED', 'MOTION WAS DETECTED ON NODE: [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM'))); }; } },

motionSMSLimiter : { label:'Motion : SMS Limited', icon:'comment', descr:'Send SMS when MOTION is detected, once per hour',
serverExecute:function(node) {
if (node.metrics['M'] && node.metrics['M'].value == 'MOTION' && (Date.now() - node.metrics['M'].updated < 2000)) /*check if M metric exists and value is MOTION, received less than 2s ago*/
{
var approveSMS = false;
if (node.metrics['M'].lastSMS) /*check if lastSMS value is not NULL ... */
{
if (Date.now() - node.metrics['M'].lastSMS > 1800000) /*check if lastSMS timestamp is more than 1hr ago*/
{
approveSMS = true;
}
}
else
{
approveSMS = true;
}

if (approveSMS)
{
node.metrics['M'].lastSMS = Date.now();
sendSMS('MOTION DETECTED', 'MOTION WAS DETECTED ON NODE: [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM')));
db.update({ _id: node._id }, { $set : node}, {}, function (err, numReplaced) { console.log(' ['+node._id+'] DB-Updates:' + numReplaced);}); /*save lastSMS timestamp to DB*/
}
else console.log(' ['+node._id+'] MOTION SMS skipped.');
};
}
},

temperatureSMSLimiter : { label:'THAlert : SMS Limited', icon:'comment', descr:'Send SMS when F>75°, once per hour',
serverExecute:function(node) {
if (node.metrics['F'] && node.metrics['F'].value > 75 && (Date.now() - node.metrics['F'].updated < 2000)) /*check if M metric exists and value is MOTION, received less than 2s ago*/
{
var approveSMS = false;
if (node.metrics['F'].lastSMS) /*check if lastSMS value is not NULL ... */
{
if (Date.now() - node.metrics['F'].lastSMS > 1800000) /*check if lastSMS timestamp is more than 1hr ago*/
{
approveSMS = true;
}
}
else
{
approveSMS = true;
}

if (approveSMS)
{
node.metrics['F'].lastSMS = Date.now();
sendSMS('Temperature > 75° !', 'Temperature alert (>75°F!): [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM')));
db.update({ _id: node._id }, { $set : node}, {}, function (err, numReplaced) { console.log(' ['+node._id+'] DB-Updates:' + numReplaced);}); /*save lastSMS timestamp to DB*/
}
else console.log(' ['+node._id+'] THAlert SMS skipped.');
};
}
},

mailboxSMS : { label:'Mailbox open : SMS', icon:'comment', descr:'Send SMS when mailbox is opened', serverExecute:function(node) { if (node.metrics['M'] && node.metrics['M'].value == 'MOTION' && (Date.now() - new Date(node.metrics['M'].updated).getTime() < 2000)) { sendSMS('MAILBOX OPENED', 'Mailbox opened [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM'))); }; } },
motionLightON23 : { label:'Motion: SM23 ON!', icon:'action', descr:'Turn SwitchMote:23 ON when MOTION is detected', serverExecute:function(node) { if (node.metrics['M'] && node.metrics['M'].value == 'MOTION' && (Date.now() - new Date(node.metrics['M'].updated).getTime() < 2000)) { sendMessageToNode({nodeId:23, action:'MOT:1'}); }; } },

Expand Down
32 changes: 20 additions & 12 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,11 @@

.hiddenNodeShow a { background-color: #ffe5e5 !important; }

#nodeList > li.ui-li-has-count > a { padding-right:2.5em; }
#nodeList > li.ui-li-has-thumb > a { padding-right:2.5em; }
@media (max-width: 599px) {
#nodeList > li.ui-li-has-count > a { padding-left: 5.25em; }
#nodeList > li.ui-li-has-count > a > span.ui-li-count {
top:70%;
}}
#nodeList > li.ui-li-has-thumb > a { padding-left: 5.25em; }
#nodeList > li.ui-li-has-thumb > a > span.ui-li-count { top:70%; }
}
@media (max-width: 767px) {
.sideButton { display:none !important; }
}
Expand Down Expand Up @@ -191,6 +190,7 @@

@media (max-width: 448px) {
label.labelbold { margin: .4em .4em .1em .4em; }
.ui-content { padding:.3em; }
}

@media (min-width: 540px) {
Expand All @@ -210,6 +210,8 @@
.nodeDetailImageWrapper { padding-left: 20%; }
#nodeDetailInputList { padding-left: 30%; padding-right: 25%; }
}


</style>
</head>
<body>
Expand Down Expand Up @@ -650,7 +652,7 @@ <h1>Settings</h1>
$("#tooltip").hide();
});

$(document).off("pageshow", "#metricdetails", renderPlot);
$(document).off("pageshow", "#metricdetails", renderAndCloneStat);
}

function refreshGraph(freezeGraph) {
Expand All @@ -675,6 +677,11 @@ <h1>Settings</h1>
function exportGraph() {
socket.emit('GETGRAPHDATA', selectedNodeId, selectedMetricKey, graphView.start, graphView.end, true);
}

function renderAndCloneStat() {
renderPlot();
$(graphStat).clone().appendTo('#metricGraph');
}

socket.on('EXPORTDATAREADY', function(rawData) {
//package and stream the data to the browser
Expand Down Expand Up @@ -708,6 +715,11 @@ <h1>Settings</h1>
min = Math.min(min, rawData.graphData.data[key].v);
}

//override autoscaleMargin and min/max from metrics graphOptions (if any defined); allows custom Y scaling of graphs
if (rawData.options.yaxis) graphOptions.yaxis.autoscaleMargin = rawData.options.yaxis.autoscaleMargin || graphOptions.yaxis.autoscaleMargin;
if (rawData.options.yaxis) min = rawData.options.yaxis.min != undefined ? rawData.options.yaxis.min : min;
if (rawData.options.yaxis) max = rawData.options.yaxis.max != undefined ? rawData.options.yaxis.max : max;

//defining the upper and lower margin
minmax=(max-min) * graphOptions.yaxis.autoscaleMargin;
if (min==max) // in case of only one value in the dataset (motion detection)
Expand All @@ -723,13 +735,9 @@ <h1>Settings</h1>
$(graphStat).html(rawData.graphData.msg != undefined ? rawData.graphData.msg : (rawData.graphData.data.length + (rawData.graphData.totalIntervalDatapoints != rawData.graphData.data.length ? ' / '+rawData.graphData.totalIntervalDatapoints : '') +'pts ('+ rawData.graphData.queryTime+'ms)'));
//need to defer plotting until after pageshow is finished rendering, otherwise the wrapper will return an incorrect width of "100"
if (metricGraphWrapper.width()==100)
$(document).on("pageshow", "#metricdetails", function() {
renderPlot();
$(graphStat).clone().appendTo('#metricGraph');
});
$(document).on("pageshow", "#metricdetails", renderAndCloneStat);
else {
renderPlot();
$(graphStat).clone().appendTo('#metricGraph');
renderAndCloneStat()
}
});

Expand Down

0 comments on commit b3e2f21

Please sign in to comment.