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.
295 lines (234 loc) • 11.3 kB
JavaScript
'use strict';
var _ = require('lodash');
var times = require('../times');
function init (ctx) {
var translate = ctx.language.translate;
var levels = ctx.levels;
var bwp = {
name: 'bwp'
, label: 'Bolus Wizard Preview'
, pluginType: 'pill-minor'
};
function checkMissingInfo (sbx) {
var errors = [];
if (!sbx.data.profile || !sbx.data.profile.hasData()) {
errors.push('Missing need a treatment profile');
} else if (profileFieldsMissing(sbx)) {
errors.push('Missing sens, target_high, or target_low treatment profile fields');
}
if (!sbx.properties.iob) {
errors.push('Missing IOB property');
}
if (!isSGVOk(sbx)) {
errors.push('Data isn\'t current');
}
return errors;
}
bwp.setProperties = function setProperties(sbx) {
sbx.offerProperty('bwp', function setBWP ( ) {
return bwp.calc(sbx);
});
};
bwp.checkNotifications = function checkNotifications (sbx) {
var prop = sbx.properties.bwp;
if (prop === undefined) { return; }
var settings = prepareSettings(sbx);
if (bwp.highSnoozedByIOB(prop, settings, sbx)) {
sbx.notifications.requestSnooze({
level: levels.URGENT
, title: translate('Snoozing high alarm since there is enough IOB')
, message: [sbx.propertyLine('bwp'), sbx.propertyLine('iob')].join('\n')
, lengthMills: settings.snoozeLength
, debug: prop
});
} else if (prop.scaledSGV > sbx.data.profile.getHighBGTarget(sbx.time) && prop.bolusEstimate > settings.warnBWP) {
var level = prop.bolusEstimate > settings.urgentBWP ? levels.URGENT : levels.WARN;
var levelLabel = levels.toDisplay(level);
var sound = level === levels.URGENT ? 'updown' : 'bike';
sbx.notifications.requestNotify({
level: level
, title: levelLabel + ', ' + translate('Check BG, time to bolus?')
, message: sbx.buildDefaultMessage()
, eventName: 'bwp'
, pushoverSound: sound
, plugin: bwp
, debug: prop
});
}
};
bwp.highSnoozedByIOB = function highSnoozedByIOB(prop, settings, sbx) {
var ar2EventType = sbx.properties.ar2 && sbx.properties.ar2.eventType;
var high = ar2EventType === 'high' || prop.scaledSGV >= sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop);
return high && prop.bolusEstimate < settings.snoozeBWP;
};
bwp.updateVisualisation = function updateVisualisation (sbx) {
var prop = sbx.properties.bwp;
var info = [];
pushInfo(prop, info, sbx);
var value;
if (prop && prop.bolusEstimateDisplay >= 0) {
value = prop.bolusEstimateDisplay + 'U';
} else if (prop && prop.bolusEstimateDisplay < 0) {
value = '< 0U';
}
sbx.pluginBase.updatePillText(bwp, {
value: value
, label: translate('BWP')
, info: info
});
};
bwp.calc = function calc (sbx) {
var results = {
effect: 0
, outcome: 0
, bolusEstimate: 0.0
};
var scaled = sbx.lastScaledSGV();
results.scaledSGV = scaled;
var errors = checkMissingInfo(sbx);
if (errors && errors.length > 0) {
results.errors = errors;
return results;
}
var profile = sbx.data.profile;
var iob = results.iob = sbx.properties.iob.iob || 0;
results.effect = iob * profile.getSensitivity(sbx.time);
results.outcome = scaled - results.effect;
var delta = 0;
var recentCarbs = _.findLast(sbx.data.treatments, function eachTreatment (treatment) {
return treatment.mills <= sbx.time &&
sbx.time - treatment.mills < times.mins(60).msecs &&
treatment.carbs > 0;
});
results.recentCarbs = recentCarbs;
var target_high = profile.getHighBGTarget(sbx.time);
var sens = profile.getSensitivity(sbx.time);
if (results.outcome > target_high) {
delta = results.outcome - target_high;
results.bolusEstimate = delta / sens;
results.aimTarget = target_high;
results.aimTargetString = 'above high';
}
var target_low = profile.getLowBGTarget(sbx.time);
results.belowLowTarget = false;
if (scaled < target_low) {
results.belowLowTarget = true;
}
if (results.outcome < target_low) {
delta = Math.abs(results.outcome - target_low);
results.bolusEstimate = delta / sens * -1;
results.aimTarget = target_low;
results.aimTargetString = 'below low';
}
if (results.bolusEstimate !== 0 && sbx.data.profile.getBasal(sbx.time)) {
// Basal profile exists, calculate % change
var basal = sbx.data.profile.getBasal(sbx.time);
var thirtyMinAdjustment = Math.round((basal/2 + results.bolusEstimate) / (basal / 2) * 100);
var oneHourAdjustment = Math.round((basal + results.bolusEstimate) / basal * 100);
results.tempBasalAdjustment = {
'thirtymin': thirtyMinAdjustment
,'onehour': oneHourAdjustment};
}
results.bolusEstimateDisplay = sbx.roundInsulinForDisplayFormat(results.bolusEstimate);
results.outcomeDisplay = sbx.roundBGToDisplayFormat(results.outcome);
results.displayIOB = sbx.roundInsulinForDisplayFormat(results.iob);
results.effectDisplay = sbx.roundBGToDisplayFormat(results.effect);
results.displayLine = translate('BWP') + ': ' + results.bolusEstimateDisplay + 'U';
return results;
};
function prepareSettings (sbx) {
return {
snoozeBWP: Number(sbx.extendedSettings.snooze) || 0.10
, warnBWP: Number(sbx.extendedSettings.warn) || 0.50
, urgentBWP: Number(sbx.extendedSettings.urgent) || 1.00
, snoozeLength: (sbx.extendedSettings.snoozeMins && Number(sbx.extendedSettings.snoozeMins) * 60 * 1000) || times.mins(10).msecs
};
}
function isSGVOk (sbx) {
var lastSGVEntry = sbx.lastSGVEntry();
return lastSGVEntry && lastSGVEntry.mgdl >= 39 && sbx.isCurrent(lastSGVEntry);
}
function profileFieldsMissing (sbx) {
return !sbx.data.profile.getSensitivity(sbx.time)
|| !sbx.data.profile.getHighBGTarget(sbx.time)
|| !sbx.data.profile.getLowBGTarget(sbx.time);
}
function pushInfo(prop, info, sbx) {
if (prop && prop.errors) {
info.push({label: translate('Notice'), value: translate('required info missing')});
_.each(prop.errors, function pushError (error) {
info.push({label: ' • ', value: error});
});
} else if (prop) {
info.push({label: translate('Insulin on Board'), value: prop.displayIOB + 'U'});
info.push({label: translate('Current target'), value: translate('Low') +': ' + sbx.data.profile.getLowBGTarget(sbx.time) + ' ' + translate('High') + ': ' + sbx.data.profile.getHighBGTarget(sbx.time)});
info.push({label: translate('Sensitivity'), value: '-' + sbx.data.profile.getSensitivity(sbx.time) + ' ' + sbx.settings.units + '/U'});
info.push({label: translate('Expected effect'), value: prop.displayIOB + ' x -' + sbx.data.profile.getSensitivity(sbx.time) + ' = -' + prop.effectDisplay + ' ' + sbx.settings.units});
info.push({label: translate('Expected outcome'), value: sbx.lastScaledSGV() + '-' + prop.effectDisplay + ' = ' + prop.outcomeDisplay + ' ' + sbx.settings.units});
if (prop.bolusEstimateDisplay < 0) {
info.unshift({label: '---------', value: ''});
var carbEquivalent = Math.ceil(Math.abs(sbx.data.profile.getCarbRatio() * prop.bolusEstimateDisplay));
info.unshift({label: translate('Carb Equivalent'), value: prop.bolusEstimateDisplay + 'U * ' + sbx.data.profile.getCarbRatio() + ' = ' + carbEquivalent + 'g'});
info.unshift({label: translate('Current Carb Ratio'), value: '1U / ' + sbx.data.profile.getCarbRatio() + ' g'});
if (prop.recentCarbs) {
info.unshift({
label: translate('Last Carbs')
, value: prop.recentCarbs.carbs + 'g @ ' + new Date(prop.recentCarbs.mills).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})});
}
if (!prop.belowLowTarget) {
info.unshift({
label: '-' + translate('BWP')
, value: translate('Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs', { params: [prop.bolusEstimateDisplay] })});
}
if (prop.belowLowTarget) {
if (prop.iob > 0) {
info.unshift({
label: '-' + translate('BWP')
, value: translate('Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS', { params: [prop.bolusEstimateDisplay] })});
} else {
info.unshift({
label: '-' + translate('BWP')
, value: translate('%1U reduction needed in active insulin to reach low target, too much basal?', { params: [prop.bolusEstimateDisplay] })});
}
}
}
} else {
info.push({label: translate('Notice'), value: translate('required info missing')});
}
pushTempBasalAdjustments(prop, info, sbx);
}
function pushTempBasalAdjustments(prop, info, sbx) {
if (prop && prop.tempBasalAdjustment) {
var carbsOrBolusMessage = translate('basal adjustment out of range, give carbs?');
var sign = '';
if (prop.tempBasalAdjustment.thirtymin > 100) {
carbsOrBolusMessage = translate('basal adjustment out of range, give bolus?');
sign = '+';
}
info.push({label: '---------', value: ''});
if (prop.aimTarget) {
info.push({label: translate('Projected BG %1 target', { params: [translate(prop.aimTargetString)] }), value: translate('aiming at') + ' ' + prop.aimTarget + ' ' + sbx.settings.units});
}
if (prop.bolusEstimate > 0) {
info.push({label: translate('Bolus %1 units', { params: [prop.bolusEstimateDisplay] }), value: translate('or adjust basal')});
info.push({label: translate('Check BG using glucometer before correcting!'), value: ''});
info.push({label: '---------', value: ''});
} else {
info.push({label: translate('Basal reduction to account %1 units:', { params: [prop.bolusEstimateDisplay] }), value: ''});
}
info.push({label: translate('Current basal'), value: sbx.data.profile.getBasal(sbx.time)});
if (prop.tempBasalAdjustment.thirtymin >= 0 && prop.tempBasalAdjustment.thirtymin <= 200) {
info.push({label: translate('30m temp basal'), value: '' + prop.tempBasalAdjustment.thirtymin + '% (' + sign + (prop.tempBasalAdjustment.thirtymin - 100) + '%)'});
} else {
info.push({label: translate('30m temp basal'), value: carbsOrBolusMessage});
}
if (prop.tempBasalAdjustment.onehour >= 0 && prop.tempBasalAdjustment.onehour <= 200) {
info.push({label: translate('1h temp basal'), value: '' + prop.tempBasalAdjustment.onehour + '% (' + sign + (prop.tempBasalAdjustment.onehour - 100) + '%)'});
} else {
info.push({label: translate('1h temp basal'), value: carbsOrBolusMessage});
}
}
}
return bwp;
}
module.exports = init;