UNPKG

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.

292 lines (224 loc) 8.02 kB
'use strict'; var _ = require('lodash'); var moment = require('moment'); var times = require('../times'); var offset = times.mins(2.5).msecs; var bucketFields = ['index', 'fromMills', 'toMills']; function init (ctx) { var translate = ctx.language.translate; var utils = require('../utils')(ctx); var bgnow = { name: 'bgnow' , label: 'BG Now' , pluginType: 'pill-primary' }; bgnow.mostRecentBucket = function mostRecentBucket (buckets) { return _.find(buckets, function notEmpty(bucket) { return bucket && !bucket.isEmpty; }); }; bgnow.previousBucket = function previousBucket(recent, buckets) { var previous = null; if (_.isObject(recent)) { previous = _.chain(buckets).find(function afterFirstNotEmpty(bucket) { return bucket.mills < recent.mills && !bucket.isEmpty; }).value(); } return previous; }; bgnow.setProperties = function setProperties (sbx) { var buckets = bgnow.fillBuckets(sbx); var recent = bgnow.mostRecentBucket(buckets); var previous = bgnow.previousBucket(recent, buckets); var delta = bgnow.calcDelta(recent, previous, sbx); sbx.offerProperty('bgnow', function setBGNow ( ) { return _.omit(recent, bucketFields); }); sbx.offerProperty('delta', function setBGNow ( ) { return delta; }); sbx.offerProperty('buckets', function setBGNow ( ) { return buckets; }); }; bgnow.fillBuckets = function fillBuckets (sbx, opts) { var bucketCount = (opts && opts.bucketCount) || 4; var bucketMins = (opts && opts.bucketMins) || 5; var bucketMsecs = times.mins(bucketMins).msecs; var lastSGVMills = sbx.lastSGVMills(); var buckets = _.times(bucketCount, function createBucket (index) { var fromMills = lastSGVMills - offset - (index * bucketMsecs); return { index: index , fromMills: fromMills , toMills: fromMills + bucketMsecs , sgvs: [ ] }; }); _.takeRightWhile(sbx.data.sgvs, function addToBucket (sgv) { //if in the future, return true and keep taking right if (sgv.mills > sbx.time) { return true; } var bucket = _.find(buckets, function containsSGV (bucket) { return sgv.mills >= bucket.fromMills && sgv.mills <= bucket.toMills; }); if (bucket) { sbx.scaleEntry(sgv); bucket.sgvs.push(sgv); } return bucket; }); return _.map(buckets, bgnow.analyzeBucket); }; function notError (entry) { return entry && entry.mgdl > 39; //TODO maybe lower instead of expecting dexcom? } function isError (entry) { return !entry || !entry.mgdl || entry.mgdl < 39; } bgnow.analyzeBucket = function analyzeBucket (bucket) { if (_.isEmpty(bucket.sgvs)) { bucket.isEmpty = true; return bucket; } var details = { }; var sgvs = _.filter(bucket.sgvs, notError); function calcMean ( ) { var sum = 0; _.forEach(sgvs, function eachSGV (sgv) { sum += Number(sgv.mgdl); }); return sum / sgvs.length; } var mean = calcMean(sgvs); if (mean && _.isNumber(mean)) { details.mean = mean; } var mostRecent = _.maxBy(sgvs, 'mills'); if (mostRecent) { details.last = mostRecent.mgdl; details.mills = mostRecent.mills; } var errors = _.filter(bucket.sgvs, isError); if (!_.isEmpty(errors)) { details.errors = errors; } return _.merge(details, bucket); }; bgnow.calcDelta = function calcDelta (recent, previous, sbx) { if (_.isEmpty(recent)) { //console.info('No recent CGM data is available'); return null; } if (_.isEmpty(previous)) { //console.info('previous bucket not found, not calculating delta'); return null; } var delta = { absolute: recent.mean - previous.mean , elapsedMins: (recent.mills - previous.mills) / times.min().msecs }; delta.interpolated = delta.elapsedMins > 9; delta.mean5MinsAgo = delta.interpolated ? recent.mean - delta.absolute / delta.elapsedMins * 5 : recent.mean - delta.absolute; delta.times = { recent: recent.mills , previous: previous.mills }; delta.mgdl = Math.round(recent.mean - delta.mean5MinsAgo); delta.scaled = sbx.settings.units === 'mmol' ? sbx.roundBGToDisplayFormat(sbx.scaleMgdl(recent.mean) - sbx.scaleMgdl(delta.mean5MinsAgo)) : delta.mgdl; delta.display = (delta.scaled >= 0 ? '+' : '') + delta.scaled; delta.previous = _.omit(previous, bucketFields); return delta; }; bgnow.updateVisualisation = function updateVisualisation (sbx) { var prop = sbx.properties.bgnow; var delta = sbx.properties.delta; var info = []; var display = delta && delta.display; if (delta && delta.interpolated) { display += ' *'; info.push({label: translate('Elapsed Time'), value: Math.round(delta.elapsedMins) + ' ' + translate('mins')}); info.push({label: translate('Absolute Delta'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.absolute)) + ' ' + sbx.unitsLabel}); info.push({label: translate('Interpolated'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.mean5MinsAgo)) + ' ' + sbx.unitsLabel}); } var deviceInfos = { }; if (prop.sgvs) { _.forEach(prop.sgvs, function deviceAndValue(entry) { var device = utils.deviceName(entry.device); deviceInfos[device] = { time: utils.timeFormat(moment(entry.mills), sbx) , value: sbx.scaleEntry(entry) , recent: entry }; }); } if (delta && delta.previous && delta.previous.sgvs) { _.forEach(delta.previous.sgvs, function deviceAndValue(entry) { var device = utils.deviceName(entry.device); var deviceInfo = deviceInfos[device]; if (deviceInfo && deviceInfo.recent) { var deviceDelta = bgnow.calcDelta( { mills: deviceInfo.recent.mills , mean: deviceInfo.recent.mgdl} , { mills: entry.mills, mean: entry.mgdl} , sbx ); if (deviceDelta) { deviceInfo.delta = deviceDelta.display } } else { deviceInfos[device] = { time: utils.timeFormat(moment(entry.mills), sbx) , value: sbx.scaleEntry(entry) }; } }); if (_.keys(deviceInfos).length > 1) { _.forIn(deviceInfos, function addInfo(deviceInfo, name) { var display = deviceInfo.value; if (deviceInfo.delta) { display += ' ' + deviceInfo.delta; } display += ' (' + deviceInfo.time + ')'; info.push({label: name, value: display}); }); } } sbx.pluginBase.updatePillText({ name: 'delta' , label: translate('BG Delta') , pluginType: 'pill-major' , pillFlip: true }, { value: display , label: sbx.unitsLabel , info: _.isEmpty(info) ? null : info }); }; function virtAsstDelta(next, slots, sbx) { var delta = sbx.properties.delta; next( translate('virtAsstTitleDelta') , translate(delta.interpolated ? 'virtAsstDeltaEstimated' : 'virtAsstDelta' , { params: [ delta.display == '+0' ? '0' : delta.display , moment(delta.times.recent).from(moment(sbx.time)) , moment(delta.times.previous).from(moment(sbx.time)) ] } ) ); } bgnow.virtAsst = { intentHandlers: [{ intent: "MetricNow" , metrics: ["delta"] , intentHandler: virtAsstDelta }] }; return bgnow; } module.exports = init;