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.
294 lines (240 loc) • 8.86 kB
JavaScript
;
var _ = require('lodash')
, moment = require('moment')
, times = require('../times');
function init(ctx) {
var translate = ctx.language.translate;
var utils = require('../utils')(ctx);
var iob = {
name: 'iob'
, label: 'Insulin-on-Board'
, pluginType: 'pill-major'
};
iob.RECENCY_THRESHOLD = times.mins(30).msecs;
iob.setProperties = function setProperties(sbx) {
sbx.offerProperty('iob', function setIOB ( ) {
return iob.calcTotal(sbx.data.treatments, sbx.data.devicestatus, sbx.data.profile, sbx.time);
});
};
iob.calcTotal = function calcTotal(treatments, devicestatus, profile, time, spec_profile) {
if (time === undefined) {
time = Date.now();
}
var result = iob.lastIOBDeviceStatus(devicestatus, time);
var treatmentResult = (treatments !== undefined && treatments.length) ? iob.fromTreatments(treatments, profile, time, spec_profile) : {};
if (_.isEmpty(result)) {
result = treatmentResult;
} else if (treatmentResult.iob) {
result.treatmentIob = +(Math.round(treatmentResult.iob + "e+3") + "e-3");
}
if (result.iob) result.iob = +(Math.round(result.iob + "e+3") + "e-3");
return addDisplay(result);
};
function addDisplay(iob) {
if (_.isEmpty(iob) || iob.iob === undefined) {
return {};
}
var display = utils.toFixed(iob.iob);
return _.merge(iob, {
display: display
, displayLine: 'IOB: ' + display + 'U'
});
}
iob.isDeviceStatusAvailable = function isDeviceStatusAvailable (devicestatus) {
return _.chain(devicestatus)
.map(iob.fromDeviceStatus)
.reject(_.isEmpty)
.value()
.length > 0;
};
iob.lastIOBDeviceStatus = function lastIOBDeviceStatus(devicestatus, time) {
if (time && time.getTime) {
time = time.getTime();
}
var futureMills = time + times.mins(5).msecs; //allow for clocks to be a little off
var recentMills = time - iob.RECENCY_THRESHOLD;
// All IOBs
var iobs = _.chain(devicestatus)
.filter(function (iobStatus) {
return iobStatus.mills <= futureMills && iobStatus.mills >= recentMills;
})
.map(iob.fromDeviceStatus)
.reject(_.isEmpty)
.sortBy('mills');
// Loop IOBs
var loopIOBs = iobs.filter(function (iobStatus) {
return iobStatus.source === 'Loop';
});
// Loop uploads both Loop IOB and pump-reported IOB, prioritize Loop IOB if available
return loopIOBs.last().value() || iobs.last().value();
};
iob.IOBDeviceStatusesInTimeRange = function IOBDeviceStatusesInTimeRange (devicestatus, from, to) {
return _.chain(devicestatus)
.filter(function (iobStatus) {
return iobStatus.mills > from && iobStatus.mills < to;
})
.map(iob.fromDeviceStatus)
.reject(_.isEmpty)
.sortBy('mills')
.value();
};
iob.fromDeviceStatus = function fromDeviceStatus(devicestatusEntry) {
var iobOpenAPS = _.get(devicestatusEntry, 'openaps.iob');
var iobLoop = _.get(devicestatusEntry, 'loop.iob');
var iobPump = _.get(devicestatusEntry, 'pump.iob');
if (_.isObject(iobOpenAPS)) {
//hacks to support AMA iob array with time fields instead of timestamp fields
iobOpenAPS = _.isArray(iobOpenAPS) ? iobOpenAPS[0] : iobOpenAPS;
// array could still be empty, handle as null
if (_.isEmpty(iobOpenAPS)) {
return {};
}
if (iobOpenAPS.time) {
iobOpenAPS.timestamp = iobOpenAPS.time;
}
return {
iob: iobOpenAPS.iob
, basaliob: iobOpenAPS.basaliob
, activity: iobOpenAPS.activity
, source: 'OpenAPS'
, device: devicestatusEntry.device
, mills: moment(iobOpenAPS.timestamp).valueOf( )
};
} else if (_.isObject(iobLoop)) {
return {
iob: iobLoop.iob
, source: 'Loop'
, device: devicestatusEntry.device
, mills: moment(iobLoop.timestamp).valueOf( )
};
} else if (_.isObject(iobPump)) {
return {
iob: iobPump.iob || iobPump.bolusiob
, source: devicestatusEntry.connect !== undefined ? 'MM Connect' : undefined
, device: devicestatusEntry.device
, mills: devicestatusEntry.mills
};
} else {
return {};
}
};
iob.fromTreatments = function fromTreatments(treatments, profile, time, spec_profile) {
var totalIOB = 0
, totalActivity = 0;
var lastBolus = null;
_.each(treatments, function eachTreatment(treatment) {
if (treatment.mills <= time) {
var tIOB = iob.calcTreatment(treatment, profile, time, spec_profile);
if (tIOB.iobContrib > 0) {
lastBolus = treatment;
}
if (tIOB && tIOB.iobContrib) { totalIOB += tIOB.iobContrib; }
// units: BG (mg/dL or mmol/L)
if (tIOB && tIOB.activityContrib) { totalActivity += tIOB.activityContrib; }
}
});
return {
iob: +(Math.round(totalIOB + "e+3") + "e-3")
, activity: totalActivity
, lastBolus: lastBolus
, source: translate('Care Portal')
};
};
iob.calcTreatment = function calcTreatment(treatment, profile, time, spec_profile) {
var dia = 3
, sens = 0;
if (profile !== undefined) {
dia = profile.getDIA(time, spec_profile) || 3;
sens = profile.getSensitivity(time, spec_profile);
}
var scaleFactor = 3.0 / dia
, peak = 75
, result = {
iobContrib: 0
, activityContrib: 0
};
if (treatment.insulin) {
var bolusTime = treatment.mills;
var minAgo = scaleFactor * (time - bolusTime) / 1000 / 60;
if (minAgo < peak) {
var x1 = minAgo / 5 + 1;
result.iobContrib = treatment.insulin * (1 - 0.001852 * x1 * x1 + 0.001852 * x1);
// units: BG (mg/dL) = (BG/U) * U insulin * scalar
result.activityContrib = sens * treatment.insulin * (2 / dia / 60 / peak) * minAgo;
} else if (minAgo < 180) {
var x2 = (minAgo - 75) / 5;
result.iobContrib = treatment.insulin * (0.001323 * x2 * x2 - 0.054233 * x2 + 0.55556);
result.activityContrib = sens * treatment.insulin * (2 / dia / 60 - (minAgo - peak) * 2 / dia / 60 / (60 * 3 - peak));
}
}
return result;
};
iob.updateVisualisation = function updateVisualisation(sbx) {
var info = [];
var prop = sbx.properties.iob;
if (prop.lastBolus) {
var when = new Date(prop.lastBolus.mills).toLocaleTimeString();
var amount = sbx.roundInsulinForDisplayFormat(Number(prop.lastBolus.insulin)) + 'U';
info.push({ label: translate('Last Bolus'), value: amount + ' @ ' + when });
}
if (prop.basaliob !== undefined) {
info.push({ label: translate('Basal IOB'), value: prop.basaliob.toFixed(2) });
}
if (prop.source !== undefined) {
info.push({ label: translate('Source'), value: prop.source });
}
if (prop.device !== undefined) {
info.push({ label: translate('Device'), value: prop.device });
}
if (prop.treatmentIob !== undefined) {
info.push({label: '------------', value: ''});
info.push({ label: translate('Careportal IOB'), value: prop.treatmentIob.toFixed(2) });
}
var value = (prop.display !== undefined ? sbx.roundInsulinForDisplayFormat(prop.display) : '---') + 'U';
sbx.pluginBase.updatePillText(iob, {
value: value
, label: translate('IOB')
, info: info
});
};
function virtAsstIOBIntentHandler (callback, slots, sbx) {
var message = translate('virtAsstIobIntent', {
params: [
getIob(sbx)
]
});
callback(translate('virtAsstTitleCurrentIOB'), message);
}
function virtAsstIOBRollupHandler (slots, sbx, callback) {
var iob = getIob(sbx);
var message = translate('virtAsstIob', {
params: [iob]
});
callback(null, {results: message, priority: 2});
}
function getIob(sbx) {
var iob = _.get(sbx, 'properties.iob.iob');
if (iob !== 0) {
return translate('virtAsstIobUnits', {
params: [
utils.toFixed(iob)
]
});
}
return translate('virtAsstNoInsulin');
}
iob.virtAsst = {
rollupHandlers: [{
rollupGroup: 'Status'
, rollupName: 'current iob'
, rollupHandler: virtAsstIOBRollupHandler
}]
, intentHandlers: [{
intent: 'MetricNow'
, metrics: ['iob', 'insulin on board']
, intentHandler: virtAsstIOBIntentHandler
}]
};
return iob;
}
module.exports = init;