Skip to content

Commit b3e2f21

Browse files
committed
8.7 release
- 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
1 parent e3d1813 commit b3e2f21

4 files changed

Lines changed: 103 additions & 28 deletions

File tree

gateway.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ io.sockets.on('connection', function (socket) {
328328
db.findOne({_id:node.nodeId}, function (err, doc) {
329329
if (doc == null)
330330
{
331-
var entry = { _id:node.nodeId, updated:(new Date).getTime(), label:node.label || 'NEW NODE', metrics:{} };
331+
var entry = { _id:node.nodeId, updated:Date.now(), label:node.label || 'NEW NODE', metrics:{} };
332332
db.insert(entry);
333333
console.log(' ['+node.nodeId+'] DB-Insert new _id:' + node.nodeId);
334334
socket.emit('LOG', 'NODE INJECTED, ID: ' + node.nodeId);
@@ -440,15 +440,15 @@ global.processSerialData = function (data) {
440440
}
441441

442442
//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
443-
if (existingNode.updated != undefined && ((new Date) - new Date(existingNode.updated).getTime()) < 500 && msgHistory[id] == msgTokens)
443+
if (existingNode.updated != undefined && (Date.now() - existingNode.updated < 500) && msgHistory[id] == msgTokens)
444444
{ console.log(" DUPLICATE, skipping..."); return; }
445445

446446
msgHistory[id] = msgTokens;
447447

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

510510
//prepare entry to save to DB, undefined values will not be saved, hence saving space
511-
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 };
511+
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 };
512512
//console.log('UPDATING ENTRY: ' + JSON.stringify(entry));
513513

514514
//save to DB
@@ -552,7 +552,7 @@ function schedule(node, eventKey) {
552552
var nextRunTimeout = metricsDef.events[eventKey].nextSchedule(node);
553553
if (nextRunTimeout < 1000)
554554
{
555-
console.ERROR('**** SCHEDULING EVENT ERROR - nodeId:' + node._id+' event:'+eventKey+' cannot schedule event in ' + nextRunTimeout + 'ms (less than 1s)');
555+
console.error('**** SCHEDULING EVENT ERROR - nodeId:' + node._id+' event:'+eventKey+' cannot schedule event in ' + nextRunTimeout + 'ms (less than 1s)');
556556
return;
557557
}
558558
var hrs = parseInt(nextRunTimeout/3600000);

logUtil.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// *******************************************************************************
2-
// This is the logging storage engine for the Moteino Gateway.
2+
// This is the logging storage engine for the Moteino IoT Gateway.
33
// It is a vast improvement over storing data in memory (previously done in neDB)
44
// http://lowpowerlab.com/gateway
55
// Some of this work was inspired by work done by Timestore and OpenEnergyMonitor:
@@ -80,14 +80,17 @@ exports.getData = function(filename, start, end, dpcount) {
8080
return {data:data, queryTime:(new Date() - ts), totalIntervalDatapoints: (posEnd-posStart)/9+1 };
8181
}
8282

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

8891
var fd;
8992
var buff = new Buffer(9);
90-
var tmp = 0, pos = 0;
93+
var lastTime = 0, lastValue = 0, pos = 0;
9194
value=Math.round(value*10000); //round to make an exactly even integer
9295

9396
//prepare 9 byte buffer to write
@@ -99,17 +102,22 @@ exports.postData = function post(filename, timestamp, value) {
99102
if (logsize>=9) {
100103
// read the last value appended to the file
101104
fd = fs.openSync(filename, 'r');
102-
var buf4 = new Buffer(4);
103-
fs.readSync(fd, buf4, 0, 4, logsize-8);
104-
tmp = buf4.readInt32BE(0); //read timestamp (bytes 1-4 bytes in buffer)
105+
var buf8 = new Buffer(8);
106+
107+
fs.readSync(fd, buf8, 0, 8, logsize-8);
108+
lastTime = buf8.readUInt32BE(0); //read timestamp (bytes 0-3 in buffer)
109+
lastValue = buf8.readInt32BE(4); //read value (bytes 4-7 in buffer)
105110
fs.closeSync(fd);
106111

107-
if (timestamp > tmp)
112+
if (timestamp > lastTime)
108113
{
109-
//timestamp is in the future, append
110-
fd = fs.openSync(filename, 'a');
111-
fs.writeSync(fd, buff, 0, 9, logsize);
112-
fs.closeSync(fd);
114+
if (value != lastValue || (timestamp-lastTime>3600)) //only write new value if different than last value or 1 hour has passed (should be a setting?)
115+
{
116+
//timestamp is in the future, append
117+
fd = fs.openSync(filename, 'a');
118+
fs.writeSync(fd, buff, 0, 9, logsize);
119+
fs.closeSync(fd);
120+
}
113121
}
114122
else
115123
{

metrics.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
// - value - this can be hardcoded, or if left blank the value will be the first captured parentheses from the regex expression
2828
// - 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
2929
// - graph:1/0 - if '1' then by default this metric will be logged in gatewayLog.db every time it comes in
30+
// - if '0' then this would not be logged but can be turned on from the metric details page
31+
// - if not defined then metric is not logged and toggle button is hidden in metric detail page
3032
// - logValue - you can specify a hardcoded value that should be logged instead of the captured metric (has to always be numeric!)
3133
// - 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
3234
// - 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
@@ -107,7 +109,7 @@ exports.metrics = {
107109
FSTATE : { name:'FSTATE', regexp:/FSTATE\:(AUTO|AUTOCIRC|ON)/i, value:''},
108110

109111
//special metrics
110-
V : { name:'V', regexp:/(?:V?BAT|VOLTS|V)\:(\d+\.\d+)v?/i, value:'', unit:'v'},
112+
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 }}},
111113
//catchAll : { name:'CatchAll', regexp:/(\w+)\:(\w+)/i, value:''},
112114
};
113115

@@ -124,6 +126,63 @@ exports.events = {
124126
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'); }; } },
125127
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'))); }; } },
126128
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'))); }; } },
129+
130+
motionSMSLimiter : { label:'Motion : SMS Limited', icon:'comment', descr:'Send SMS when MOTION is detected, once per hour',
131+
serverExecute:function(node) {
132+
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*/
133+
{
134+
var approveSMS = false;
135+
if (node.metrics['M'].lastSMS) /*check if lastSMS value is not NULL ... */
136+
{
137+
if (Date.now() - node.metrics['M'].lastSMS > 1800000) /*check if lastSMS timestamp is more than 1hr ago*/
138+
{
139+
approveSMS = true;
140+
}
141+
}
142+
else
143+
{
144+
approveSMS = true;
145+
}
146+
147+
if (approveSMS)
148+
{
149+
node.metrics['M'].lastSMS = Date.now();
150+
sendSMS('MOTION DETECTED', 'MOTION WAS DETECTED ON NODE: [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM')));
151+
db.update({ _id: node._id }, { $set : node}, {}, function (err, numReplaced) { console.log(' ['+node._id+'] DB-Updates:' + numReplaced);}); /*save lastSMS timestamp to DB*/
152+
}
153+
else console.log(' ['+node._id+'] MOTION SMS skipped.');
154+
};
155+
}
156+
},
157+
158+
temperatureSMSLimiter : { label:'THAlert : SMS Limited', icon:'comment', descr:'Send SMS when F>75°, once per hour',
159+
serverExecute:function(node) {
160+
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*/
161+
{
162+
var approveSMS = false;
163+
if (node.metrics['F'].lastSMS) /*check if lastSMS value is not NULL ... */
164+
{
165+
if (Date.now() - node.metrics['F'].lastSMS > 1800000) /*check if lastSMS timestamp is more than 1hr ago*/
166+
{
167+
approveSMS = true;
168+
}
169+
}
170+
else
171+
{
172+
approveSMS = true;
173+
}
174+
175+
if (approveSMS)
176+
{
177+
node.metrics['F'].lastSMS = Date.now();
178+
sendSMS('Temperature > 75° !', 'Temperature alert (>75°F!): [' + node._id + ':' + node.label + '] @ ' + (new Date().toLocaleTimeString() + (new Date().getHours() > 12 ? 'PM':'AM')));
179+
db.update({ _id: node._id }, { $set : node}, {}, function (err, numReplaced) { console.log(' ['+node._id+'] DB-Updates:' + numReplaced);}); /*save lastSMS timestamp to DB*/
180+
}
181+
else console.log(' ['+node._id+'] THAlert SMS skipped.');
182+
};
183+
}
184+
},
185+
127186
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'))); }; } },
128187
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'}); }; } },
129188

www/index.html

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,11 @@
145145

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

148-
#nodeList > li.ui-li-has-count > a { padding-right:2.5em; }
148+
#nodeList > li.ui-li-has-thumb > a { padding-right:2.5em; }
149149
@media (max-width: 599px) {
150-
#nodeList > li.ui-li-has-count > a { padding-left: 5.25em; }
151-
#nodeList > li.ui-li-has-count > a > span.ui-li-count {
152-
top:70%;
153-
}}
150+
#nodeList > li.ui-li-has-thumb > a { padding-left: 5.25em; }
151+
#nodeList > li.ui-li-has-thumb > a > span.ui-li-count { top:70%; }
152+
}
154153
@media (max-width: 767px) {
155154
.sideButton { display:none !important; }
156155
}
@@ -191,6 +190,7 @@
191190

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

196196
@media (min-width: 540px) {
@@ -210,6 +210,8 @@
210210
.nodeDetailImageWrapper { padding-left: 20%; }
211211
#nodeDetailInputList { padding-left: 30%; padding-right: 25%; }
212212
}
213+
214+
213215
</style>
214216
</head>
215217
<body>
@@ -650,7 +652,7 @@ <h1>Settings</h1>
650652
$("#tooltip").hide();
651653
});
652654

653-
$(document).off("pageshow", "#metricdetails", renderPlot);
655+
$(document).off("pageshow", "#metricdetails", renderAndCloneStat);
654656
}
655657

656658
function refreshGraph(freezeGraph) {
@@ -675,6 +677,11 @@ <h1>Settings</h1>
675677
function exportGraph() {
676678
socket.emit('GETGRAPHDATA', selectedNodeId, selectedMetricKey, graphView.start, graphView.end, true);
677679
}
680+
681+
function renderAndCloneStat() {
682+
renderPlot();
683+
$(graphStat).clone().appendTo('#metricGraph');
684+
}
678685

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

718+
//override autoscaleMargin and min/max from metrics graphOptions (if any defined); allows custom Y scaling of graphs
719+
if (rawData.options.yaxis) graphOptions.yaxis.autoscaleMargin = rawData.options.yaxis.autoscaleMargin || graphOptions.yaxis.autoscaleMargin;
720+
if (rawData.options.yaxis) min = rawData.options.yaxis.min != undefined ? rawData.options.yaxis.min : min;
721+
if (rawData.options.yaxis) max = rawData.options.yaxis.max != undefined ? rawData.options.yaxis.max : max;
722+
711723
//defining the upper and lower margin
712724
minmax=(max-min) * graphOptions.yaxis.autoscaleMargin;
713725
if (min==max) // in case of only one value in the dataset (motion detection)
@@ -723,13 +735,9 @@ <h1>Settings</h1>
723735
$(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)'));
724736
//need to defer plotting until after pageshow is finished rendering, otherwise the wrapper will return an incorrect width of "100"
725737
if (metricGraphWrapper.width()==100)
726-
$(document).on("pageshow", "#metricdetails", function() {
727-
renderPlot();
728-
$(graphStat).clone().appendTo('#metricGraph');
729-
});
738+
$(document).on("pageshow", "#metricdetails", renderAndCloneStat);
730739
else {
731-
renderPlot();
732-
$(graphStat).clone().appendTo('#metricGraph');
740+
renderAndCloneStat()
733741
}
734742
});
735743

0 commit comments

Comments
 (0)