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.
175 lines (138 loc) • 6.7 kB
JavaScript
;
const _ = require('lodash');
const times = require('../times');
const crypto = require('crypto');
const MANUAL_TREATMENTS = ['BG Check', 'Meal Bolus', 'Carb Correction', 'Correction Bolus'];
function init(ctx) {
const treatmentnotify = {
name: 'treatmentnotify'
, label: 'Treatment Notifications'
, pluginType: 'notification'
};
const simplealarms = require('./simplealarms')(ctx);
//automated treatments from OpenAPS or Loop shouldn't trigger notifications or snooze alarms
function filterTreatments (sbx) {
var treatments = sbx.data.treatments;
var includeBolusesOver = sbx.extendedSettings.includeBolusesOver || 0;
treatments = _.filter(treatments, function notOpenAPS (treatment) {
var ok = true;
var enteredBy = treatment.enteredBy;
if (enteredBy && (enteredBy.indexOf('openaps://') === 0 || enteredBy.indexOf('loop://') === 0)) {
ok = _.indexOf(MANUAL_TREATMENTS, treatment.eventType) >= 0;
}
if (ok && _.isNumber(treatment.insulin) && _.includes(['Meal Bolus', 'Correction Bolus'], treatment.eventType)) {
ok = treatment.insulin >= includeBolusesOver;
}
return ok;
});
return treatments;
}
treatmentnotify.checkNotifications = function checkNotifications (sbx) {
var treatments = filterTreatments(sbx);
var lastMBG = sbx.lastEntry(sbx.data.mbgs);
var lastTreatment = sbx.lastEntry(treatments);
var mbgCurrent = isCurrent(lastMBG);
var treatmentCurrent = isCurrent(lastTreatment);
var translate = sbx.language.translate;
if (mbgCurrent || treatmentCurrent) {
var mbgMessage = mbgCurrent ? translate('Meter BG') +' ' + sbx.scaleEntry(lastMBG) + ' ' + sbx.unitsLabel : '';
var treatmentMessage = treatmentCurrent ? translate('Treatment') + ': ' + lastTreatment.eventType : '';
autoSnoozeAlarms(mbgMessage, treatmentMessage, lastTreatment, sbx);
//and add some info notifications
//the notification providers (push, websockets, etc) are responsible to not sending the same notifications repeatedly
if (mbgCurrent) { requestMBGNotify(lastMBG, sbx); }
if (treatmentCurrent) {
requestTreatmentNotify(lastTreatment, sbx);
}
}
};
function autoSnoozeAlarms(mbgMessage, treatmentMessage, lastTreatment, sbx) {
//announcements don't snooze alarms
if (lastTreatment && !lastTreatment.isAnnouncement) {
var snoozeLength = sbx.extendedSettings.snoozeMins && times.mins(sbx.extendedSettings.snoozeMins).msecs || times.mins(10).msecs;
sbx.notifications.requestSnooze({
level: sbx.levels.URGENT
, title: 'Snoozing alarms since there was a recent treatment'
, message: _.trim([mbgMessage, treatmentMessage].join('\n'))
, lengthMills: snoozeLength
});
}
}
function requestMBGNotify (lastMBG, sbx) {
console.info('requestMBGNotify for', lastMBG);
var translate = sbx.language.translate;
sbx.notifications.requestNotify({
level: sbx.levels.INFO
, title: 'Calibration' //assume all MGBs are calibrations for now
, message: translate('Meter BG') + ': ' + sbx.scaleEntry(lastMBG) + ' ' + sbx.unitsLabel
, plugin: treatmentnotify
, pushoverSound: 'magic'
});
}
function requestAnnouncementNotify (lastTreatment, sbx) {
var result = simplealarms.compareBGToTresholds(sbx.scaleMgdl(lastTreatment.mgdl), sbx);
sbx.notifications.requestNotify({
level: result.level
, title: (result.level === sbx.levels.URGENT ? sbx.levels.toDisplay(sbx.levels.URGENT) + ' ' : '') + lastTreatment.eventType
, message: lastTreatment.notes || '.' //some message is required
, plugin: treatmentnotify
, group: 'Announcement'
, pushoverSound: sbx.levels.isAlarm(result.level) ? result.pushoverSound : undefined
, isAnnouncement: true
});
}
function requestTreatmentNotify (lastTreatment, sbx) {
var translate = sbx.language.translate;
if (lastTreatment.isAnnouncement) {
requestAnnouncementNotify(lastTreatment, sbx);
} else {
let message = buildTreatmentMessage(lastTreatment, sbx);
let eventType = lastTreatment.eventType;
if (lastTreatment.duration === 0 && eventType === 'Temporary Target') {
eventType += ' Cancel';
message = translate('Canceled');
}
const timestamp = lastTreatment.timestamp;
if (!message) {
message = '...';
}
const hash = crypto.createHash('sha1');
const info = JSON.stringify({ eventType, timestamp});
hash.update(info);
const notifyhash = hash.digest('hex');
sbx.notifications.requestNotify({
level: sbx.levels.INFO
, title: translate(eventType)
, message
, timestamp
, plugin: treatmentnotify
, notifyhash
});
}
}
function buildTreatmentMessage(lastTreatment, sbx) {
var translate = sbx.language.translate;
return (lastTreatment.glucose ? translate('BG') + ': ' + lastTreatment.glucose + ' (' + lastTreatment.glucoseType + ')' : '') +
(lastTreatment.reason ? '\n' + translate('Reason') + ': ' + lastTreatment.reason : '') +
(lastTreatment.targetTop ? '\n' + translate('Target Top') + ': ' + lastTreatment.targetTop : '') +
(lastTreatment.targetBottom ? '\n' + translate('Target Bottom') + ': ' + lastTreatment.targetBottom : '') +
(lastTreatment.carbs ? '\n' + translate('Carbs') + ': ' + lastTreatment.carbs + 'g' : '') +
(lastTreatment.insulin ? '\n' + translate('Insulin') + ': ' + sbx.roundInsulinForDisplayFormat(lastTreatment.insulin) + 'U' : '')+
(lastTreatment.duration ? '\n' + translate('Duration') + ': ' + lastTreatment.duration + ' min' : '')+
(lastTreatment.percent ? '\n' + translate('Percent') + ': ' + (lastTreatment.percent > 0 ? '+' : '') + lastTreatment.percent + '%' : '')+
(!isNaN(lastTreatment.absolute) ? '\n' + translate('Value') + ': ' + lastTreatment.absolute + 'U' : '')+
(lastTreatment.enteredBy ? '\n' + translate('Entered By') + ': ' + lastTreatment.enteredBy : '') +
(lastTreatment.notes ? '\n' + translate('Notes') + ': ' + lastTreatment.notes : '');
}
return treatmentnotify;
}
function isCurrent(last) {
if (!last) {
return false;
}
var now = Date.now();
var lastTime = last.mills;
var ago = (last.mills <= now) ? now - lastTime : -1;
return ago !== -1 && ago < times.mins(10).msecs;
}
module.exports = init;