nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
271 lines (218 loc) • 8.16 kB
JavaScript
;
var _ = require('lodash');
var THIRTY_MINUTES = 30 * 60 * 1000;
var DEFAULT_GROUPS = ['default'];
var Alarm = function(level, group, label) {
this.level = level;
this.group = group;
this.label = label;
this.silenceTime = THIRTY_MINUTES;
this.lastAckTime = 0;
};
// list of alarms with their thresholds
var alarms = {};
function init (env, ctx) {
function notifications () {
return notifications;
}
function getAlarm (level, group) {
var key = level + '-' + group;
var alarm = alarms[key];
if (!alarm) {
var display = group === 'default' ? ctx.levels.toDisplay(level) : group + ':' + level;
alarm = new Alarm(level, group, display);
alarms[key] = alarm;
}
return alarm;
}
//should only be used when auto acking the alarms after going back in range or when an error corrects
//setting the silence time to 1ms so the alarm will be re-triggered as soon as the condition changes
//since this wasn't ack'd by a user action
function autoAckAlarms (group) {
var sendClear = false;
for (var level = 1; level <= 2; level++) {
var alarm = getAlarm(level, group);
if (alarm.lastEmitTime) {
console.info('auto acking ' + alarm.level, ' - ', group);
notifications.ack(alarm.level, group, 1);
sendClear = true;
}
}
if (sendClear) {
var notify = { clear: true, title: 'All Clear', message: 'Auto ack\'d alarm(s)', group: group };
ctx.bus.emit('notification', notify);
logEmitEvent(notify);
}
}
function emitNotification (notify) {
var alarm = getAlarm(notify.level, notify.group);
if (ctx.ddata.lastUpdated > alarm.lastAckTime + alarm.silenceTime) {
ctx.bus.emit('notification', notify);
alarm.lastEmitTime = ctx.ddata.lastUpdated;
logEmitEvent(notify);
} else {
console.log(alarm.label + ' alarm is silenced for ' + Math.floor((alarm.silenceTime - (ctx.ddata.lastUpdated - alarm.lastAckTime)) / 60000) + ' minutes more');
}
}
var requests = {};
notifications.initRequests = function initRequests () {
requests = { notifies: [], snoozes: [] };
};
notifications.initRequests();
/**
* Find the first URGENT or first WARN
* @returns a notification or undefined
*/
notifications.findHighestAlarm = function findHighestAlarm (group) {
group = group || 'default';
var filtered = _.filter(requests.notifies, { group: group });
return _.find(filtered, { level: ctx.levels.URGENT }) || _.find(filtered, { level: ctx.levels.WARN });
};
notifications.findUnSnoozeable = function findUnSnoozeable () {
return _.filter(requests.notifies, function(notify) {
return notify.level <= ctx.levels.INFO || notify.isAnnouncement;
});
};
notifications.snoozedBy = function snoozedBy (notify) {
if (notify.isAnnouncement) { return false; }
var filtered = _.filter(requests.snoozes, { group: notify.group });
if (_.isEmpty(filtered)) { return false; }
var byLevel = _.filter(filtered, function checkSnooze (snooze) {
return snooze.level >= notify.level;
});
var sorted = _.sortBy(byLevel, 'lengthMills');
return _.last(sorted);
};
notifications.requestNotify = function requestNotify (notify) {
if (!Object.prototype.hasOwnProperty.call(notify, 'level') || !notify.title || !notify.message || !notify.plugin) {
console.error(new Error('Unable to request notification, since the notify isn\'t complete: ' + JSON.stringify(notify)));
return;
}
notify.group = notify.group || 'default';
requests.notifies.push(notify);
};
notifications.requestSnooze = function requestSnooze (snooze) {
if (!snooze.level || !snooze.title || !snooze.message || !snooze.lengthMills) {
console.error(new Error('Unable to request snooze, since the snooze isn\'t complete: ' + JSON.stringify(snooze)));
return;
}
snooze.group = snooze.group || 'default';
requests.snoozes.push(snooze);
};
notifications.process = function process () {
var notifyGroups = _.map(requests.notifies, function eachNotify (notify) {
return notify.group;
});
var alarmGroups = _.map(_.values(alarms), function eachAlarm (alarm) {
return alarm.group;
});
var groups = _.uniq(notifyGroups.concat(alarmGroups));
if (_.isEmpty(groups)) {
groups = DEFAULT_GROUPS.slice();
}
_.each(groups, function eachGroup (group) {
var highestAlarm = notifications.findHighestAlarm(group);
if (highestAlarm) {
var snoozedBy = notifications.snoozedBy(highestAlarm, group);
if (snoozedBy) {
logSnoozingEvent(highestAlarm, snoozedBy);
notifications.ack(snoozedBy.level, group, snoozedBy.lengthMills, true);
} else {
emitNotification(highestAlarm);
}
} else {
autoAckAlarms(group);
}
});
notifications.findUnSnoozeable().forEach(function eachInfo (notify) {
emitNotification(notify);
});
};
notifications.ack = function ack (level, group, time, sendClear) {
var alarm = getAlarm(level, group);
if (!alarm) {
console.warn('Got an ack for an unknown alarm time, level:', level, ', group:', group);
return;
}
if (Date.now() < alarm.lastAckTime + alarm.silenceTime) {
console.warn('Alarm has already been snoozed, don\'t snooze it again, level:', level, ', group:', group);
return;
}
alarm.lastAckTime = Date.now();
alarm.silenceTime = time ? time : THIRTY_MINUTES;
delete alarm.lastEmitTime;
if (level === 2) {
notifications.ack(1, group, time);
}
if (sendClear) {
var notify = {
clear: true
, title: 'All Clear'
, message: group + ' - ' + ctx.levels.toDisplay(level) + ' was ack\'d'
, group: group
};
ctx.bus.emit('notification', notify);
logEmitEvent(notify);
}
};
function ifTestModeThen (callback) {
if (env.testMode) {
return callback();
} else {
throw 'Test only function was called = while not in test mode';
}
}
notifications.resetStateForTests = function resetStateForTests () {
ifTestModeThen(function doResetStateForTests () {
console.info('resetting notifications state for tests');
alarms = {};
});
};
notifications.getAlarmForTests = function getAlarmForTests (level, group) {
return ifTestModeThen(function doResetStateForTests () {
group = group || 'default';
var alarm = getAlarm(level, group);
console.info('got alarm for tests: ', alarm);
return alarm;
});
};
function notifyToView (notify) {
return {
level: ctx.levels.toDisplay(notify.level)
, title: notify.title
, message: notify.message
, group: notify.group
, plugin: notify.plugin ? notify.plugin.name : '<none>'
, debug: notify.debug
};
}
function snoozeToView (snooze) {
return {
level: ctx.levels.toDisplay(snooze.level)
, title: snooze.title
, message: snooze.message
, group: snooze.group
};
}
function logEmitEvent (notify) {
var type = notify.level >= ctx.levels.WARN ? 'ALARM' : (notify.clear ? 'ALL CLEAR' : 'NOTIFICATION');
console.info([
logTimestamp() + '\tEMITTING ' + type + ':'
, ' ' + JSON.stringify(notifyToView(notify))
].join('\n'));
}
function logSnoozingEvent (highestAlarm, snoozedBy) {
console.info([
logTimestamp() + '\tSNOOZING ALARM:'
, ' ' + JSON.stringify(notifyToView(highestAlarm))
, ' BECAUSE:'
, ' ' + JSON.stringify(snoozeToView(snoozedBy))
].join('\n'));
}
//TODO: we need a common logger, but until then...
function logTimestamp () {
return (new Date).toISOString();
}
return notifications();
}
module.exports = init;