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.

300 lines (253 loc) 9.45 kB
'use strict'; var _ = require('lodash'); var times = require('../times'); var consts = require('../constants'); var DEVICE_TYPE_FIELDS = ['uploader', 'pump', 'openaps', 'loop', 'xdripjs']; function init () { var ddata = { sgvs: [] , treatments: [] , mbgs: [] , cals: [] , profiles: [] , devicestatus: [] , food: [] , activity: [] , dbstats: {} , lastUpdated: 0 }; /** * Convert Mongo ids to strings and ensure all objects have the mills property for * significantly faster processing than constant date parsing, plus simplified * logic */ ddata.processRawDataForRuntime = (data) => { let obj = _.cloneDeep(data); Object.keys(obj).forEach(key => { if (typeof obj[key] === 'object' && obj[key]) { if (Object.prototype.hasOwnProperty.call(obj[key], '_id')) { obj[key]._id = obj[key]._id.toString(); } if (Object.prototype.hasOwnProperty.call(obj[key], 'created_at') && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { obj[key].mills = new Date(obj[key].created_at).getTime(); } if (Object.prototype.hasOwnProperty.call(obj[key], 'sysTime') && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { obj[key].mills = new Date(obj[key].sysTime).getTime(); } } }); return obj; }; /** * Merge two arrays based on _id string, preferring new objects when a collision is found * @param {array} oldData * @param {array} newData */ ddata.idMergePreferNew = (oldData, newData) => { if (!newData && oldData) return oldData; if (!oldData && newData) return newData; const merged = _.cloneDeep(newData); for (let i = 0; i < oldData.length; i++) { const oldElement = oldData[i]; let found = false; for (let j = 0; j < newData.length; j++) { if (oldElement._id == newData[j]._id) { found = true; break; } } if (!found) merged.push(oldElement); // Merge old object in, if it wasn't found in the new data } return merged; }; ddata.clone = function clone () { return _.clone(ddata, function(value) { //special handling of mongo ObjectID's //see https://github.com/lodash/lodash/issues/602#issuecomment-47414964 //instead of requiring Mongo.ObjectID here and having it get pulled into the bundle //we'll look for the toHexString function and then assume it's an ObjectID if (value && value.toHexString && value.toHexString.call && value.toString && value.toString.call) { return value.toString(); } }); }; ddata.dataWithRecentStatuses = function dataWithRecentStatuses () { var results = {}; results.devicestatus = ddata.recentDeviceStatus(Date.now()); results.sgvs = ddata.sgvs; results.cals = ddata.cals; var profiles = _.cloneDeep(ddata.profiles); if (profiles && profiles[0] && profiles[0].store) { Object.keys(profiles[0].store).forEach(k => { if (k.indexOf('@@@@@') > 0) { delete profiles[0].store[k]; } }) } results.profiles = profiles; results.mbgs = ddata.mbgs; results.food = ddata.food; results.treatments = ddata.treatments; results.dbstats = ddata.dbstats; return results; } ddata.recentDeviceStatus = function recentDeviceStatus (time) { var deviceAndTypes = _.chain(ddata.devicestatus) .map(function eachStatus (status) { return _.chain(status) .keys() .filter(function isExcluded (key) { return _.includes(DEVICE_TYPE_FIELDS, key); }) .map(function toDeviceTypeKey (key) { return { device: status.device , type: key }; }) .value(); }) .flatten() .uniqWith(_.isEqual) .value(); //console.info('>>>deviceAndTypes', deviceAndTypes); var rv = _.chain(deviceAndTypes) .map(function findMostRecent (deviceAndType) { return _.chain(ddata.devicestatus) .filter(function isSameDeviceType (status) { return status.device === deviceAndType.device && _.has(status, deviceAndType.type) }) .filter(function notInTheFuture (status) { return status.mills <= time; }) .sortBy('mills') .takeRight(10) .value(); }).value(); var merged = [].concat.apply([], rv); rv = _.chain(merged) .filter(_.isObject) .uniq('_id') .sortBy('mills') .value(); return rv; }; ddata.processDurations = function processDurations (treatments, keepzeroduration) { treatments = _.uniqBy(treatments, 'mills'); // cut temp basals by end events // better to do it only on data update var endevents = treatments.filter(function filterEnd (t) { return !t.duration; }); function cutIfInInterval (base, end) { if (base.mills < end.mills && base.mills + times.mins(base.duration).msecs > end.mills) { base.duration = times.msecs(end.mills - base.mills).mins; if (end.profile) { base.cuttedby = end.profile; end.cutting = base.profile; } } } // cut by end events treatments.forEach(function allTreatments (t) { if (t.duration) { endevents.forEach(function allEndevents (e) { cutIfInInterval(t, e); }); } }); // cut by overlaping events treatments.forEach(function allTreatments (t) { if (t.duration) { treatments.forEach(function allEndevents (e) { cutIfInInterval(t, e); }); } }); if (keepzeroduration) { return treatments; } else { return treatments.filter(function filterEnd (t) { return t.duration; }); } }; ddata.processTreatments = function processTreatments (preserveOrignalTreatments) { // filter & prepare 'Site Change' events ddata.sitechangeTreatments = ddata.treatments.filter(function filterSensor (t) { return t.eventType.indexOf('Site Change') > -1; }).sort(function(a, b) { return a.mills > b.mills; }); // filter & prepare 'Insulin Change' events ddata.insulinchangeTreatments = ddata.treatments.filter(function filterInsulin (t) { return t.eventType.indexOf('Insulin Change') > -1; }).sort(function(a, b) { return a.mills > b.mills; }); // filter & prepare 'Pump Battery Change' events ddata.batteryTreatments = ddata.treatments.filter(function filterSensor (t) { return t.eventType.indexOf('Pump Battery Change') > -1; }).sort(function(a, b) { return a.mills > b.mills; }); // filter & prepare 'Sensor' events ddata.sensorTreatments = ddata.treatments.filter(function filterSensor (t) { return t.eventType.indexOf('Sensor') > -1; }).sort(function(a, b) { return a.mills > b.mills; }); // filter & prepare 'Profile Switch' events var profileTreatments = ddata.treatments.filter(function filterProfiles (t) { return t.eventType === 'Profile Switch'; }).sort(function(a, b) { return a.mills > b.mills; }); if (preserveOrignalTreatments) profileTreatments = _.cloneDeep(profileTreatments); ddata.profileTreatments = ddata.processDurations(profileTreatments, true); // filter & prepare 'Combo Bolus' events ddata.combobolusTreatments = ddata.treatments.filter(function filterComboBoluses (t) { return t.eventType === 'Combo Bolus'; }).sort(function(a, b) { return a.mills > b.mills; }); // filter & prepare temp basals var tempbasalTreatments = ddata.treatments.filter(function filterBasals (t) { return t.eventType && t.eventType.indexOf('Temp Basal') > -1; }); if (preserveOrignalTreatments) tempbasalTreatments = _.cloneDeep(tempbasalTreatments); ddata.tempbasalTreatments = ddata.processDurations(tempbasalTreatments, false); // filter temp target var tempTargetTreatments = ddata.treatments.filter(function filterTargets (t) { //check for a units being sent if (t.units) { if (t.units == 'mmol') { //convert to mgdl t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } } //if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety. if (t.targetTop < 20) { t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } if (t.targetBottom < 20) { t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } return t.eventType && t.eventType.indexOf('Temporary Target') > -1; }); if (preserveOrignalTreatments) tempTargetTreatments = _.cloneDeep(tempTargetTreatments); ddata.tempTargetTreatments = ddata.processDurations(tempTargetTreatments, false); }; return ddata; } module.exports = init;