Browse Source

oref0: minimised and source files from open-iaps-oref dev 97b012a

https://github.com/nightscout/open-iaps-oref/tree/97b012ab8e8f0062cecc9d2e02d915ef0124cd2e
bjornoleh 2 years ago
parent
commit
5cdaa3ec73
58 changed files with 7820 additions and 9 deletions
  1. 1 1
      FreeAPS/Resources/javascript/bundle/autosens.js
  2. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-core.js
  3. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-prep.js
  4. 1 1
      FreeAPS/Resources/javascript/bundle/basal-set-temp.js
  5. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  6. 1 1
      FreeAPS/Resources/javascript/bundle/glucose-get-last.js
  7. 1 1
      FreeAPS/Resources/javascript/bundle/iob.js
  8. 1 1
      FreeAPS/Resources/javascript/bundle/meal.js
  9. 1 1
      FreeAPS/Resources/javascript/bundle/profile.js
  10. 451 0
      oref0/lib/lib/autotune-prep/categorize.js
  11. 26 0
      oref0/lib/lib/autotune-prep/dosed.js
  12. 175 0
      oref0/lib/lib/autotune-prep/index.js
  13. 554 0
      oref0/lib/lib/autotune/index.js
  14. 60 0
      oref0/lib/lib/basal-set-temp.js
  15. 180 0
      oref0/lib/lib/bolus.js
  16. 30 0
      oref0/lib/lib/calc-glucose-stats.js
  17. 451 0
      oref0/lib/lib/determine-basal/autosens.js
  18. 205 0
      oref0/lib/lib/determine-basal/cob.js
  19. 2053 0
      oref0/lib/lib/determine-basal/determine-basal.js
  20. 329 0
      oref0/lib/lib/glucose-get-last.js
  21. 246 0
      oref0/lib/lib/glucose-stats.js
  22. 144 0
      oref0/lib/lib/iob/calculate.js
  23. 570 0
      oref0/lib/lib/iob/history.js
  24. 84 0
      oref0/lib/lib/iob/index.js
  25. 103 0
      oref0/lib/lib/iob/total.js
  26. 144 0
      oref0/lib/lib/meal/history.js
  27. 23 0
      oref0/lib/lib/meal/index.js
  28. 142 0
      oref0/lib/lib/meal/total.js
  29. 12 0
      oref0/lib/lib/medtronic-clock.js
  30. 9 0
      oref0/lib/lib/oref0-setup/alias.json
  31. 36 0
      oref0/lib/lib/oref0-setup/autotune.json
  32. 50 0
      oref0/lib/lib/oref0-setup/basal_profile.json
  33. 24 0
      oref0/lib/lib/oref0-setup/bg_targets_raw.json
  34. 270 0
      oref0/lib/lib/oref0-setup/cgm-loop.json
  35. 131 0
      oref0/lib/lib/oref0-setup/device.json
  36. 11 0
      oref0/lib/lib/oref0-setup/dexcom.json
  37. 2 0
      oref0/lib/lib/oref0-setup/edisonbattery.json
  38. 113 0
      oref0/lib/lib/oref0-setup/mdt-cgm.json
  39. 35 0
      oref0/lib/lib/oref0-setup/pancreabble.json
  40. 7 0
      oref0/lib/lib/oref0-setup/pancreoptions.json
  41. 354 0
      oref0/lib/lib/oref0-setup/report.json
  42. 8 0
      oref0/lib/lib/oref0-setup/settings.json
  43. 19 0
      oref0/lib/lib/oref0-setup/shareble.json
  44. 22 0
      oref0/lib/lib/oref0-setup/supermicrobolus.json
  45. 18 0
      oref0/lib/lib/oref0-setup/vendor.json
  46. 26 0
      oref0/lib/lib/oref0-setup/xdrip-cgm.json
  47. 17 0
      oref0/lib/lib/percentile.js
  48. 45 0
      oref0/lib/lib/profile/basal.js
  49. 38 0
      oref0/lib/lib/profile/carbs.js
  50. 198 0
      oref0/lib/lib/profile/index.js
  51. 47 0
      oref0/lib/lib/profile/isf.js
  52. 84 0
      oref0/lib/lib/profile/targets.js
  53. 35 0
      oref0/lib/lib/pump.js
  54. 81 0
      oref0/lib/lib/require-utils.js
  55. 44 0
      oref0/lib/lib/round-basal.js
  56. 48 0
      oref0/lib/lib/temps.js
  57. 56 0
      oref0/lib/lib/with-raw-glucose.js
  58. 1 0
      oref0_source_version.txt

File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-core.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/basal-set-temp.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/glucose-get-last.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/meal.js


File diff suppressed because it is too large
+ 1 - 1
FreeAPS/Resources/javascript/bundle/profile.js


+ 451 - 0
oref0/lib/lib/autotune-prep/categorize.js

@@ -0,0 +1,451 @@
+var tz = require('moment-timezone');
+var basal = require('../profile/basal');
+var getIOB = require('../iob');
+var ISF = require('../profile/isf');
+var find_insulin = require('../iob/history');
+var dosed = require('./dosed');
+
+// main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals.
+
+function categorizeBGDatums(opts) {
+    var treatments = opts.treatments;
+    // this sorts the treatments collection in order.
+    treatments.sort(function (a, b) {
+        var aDate = new Date(tz(a.timestamp));
+        var bDate = new Date(tz(b.timestamp));
+        //console.error(aDate);
+        return bDate.getTime() - aDate.getTime();
+    });
+    var profileData = opts.profile;
+
+    var glucoseData = [ ];
+    if (typeof(opts.glucose) !== 'undefined') {
+        //var glucoseData = opts.glucose;
+        glucoseData = opts.glucose.map(function prepGlucose (obj) {
+            //Support the NS sgv field to avoid having to convert in a custom way
+            obj.glucose = obj.glucose || obj.sgv;
+
+            if (obj.date) {
+                //obj.BGTime = new Date(obj.date);
+            } else if (obj.displayTime) {
+                // Attempt to get date from displayTime
+                obj.date = new Date(obj.displayTime.replace('T', ' ')).getTime();
+            } else if (obj.dateString) {
+                // Attempt to get date from dateString
+                obj.date = new Date(obj.dateString).getTime();
+            }// else { console.error("Could not determine BG time"); }
+
+            if (!obj.dateString)
+            {
+                obj.dateString = new Date(tz(obj.date)).toISOString();
+            }
+            return obj;
+        }).filter(function filterRecords(obj) {
+            // Only take records with a valid date record
+            // and a glucose value, which is also above 39
+            return (obj.date && obj.glucose && obj.glucose >=39);
+        }).sort(function (a, b) {
+            // sort the collection in order
+            return b.date - a.date;
+        });
+    }
+    // if (typeof(opts.preppedGlucose) !== 'undefined') {
+        // var preppedGlucoseData = opts.preppedGlucose;
+    // }
+    //starting variable at 0
+    var boluses = 0;
+    var maxCarbs = 0;
+    //console.error(treatments);
+    if (!treatments) return {};
+
+    //console.error(glucoseData);
+    var IOBInputs = {
+        profile: profileData
+    ,   history: opts.pumpHistory
+    };
+    var CSFGlucoseData = [];
+    var ISFGlucoseData = [];
+    var basalGlucoseData = [];
+    var UAMGlucoseData = [];
+    var CRData = [];
+
+    var bucketedData = [];
+    bucketedData[0] = JSON.parse(JSON.stringify(glucoseData[0]));
+    var j=0;
+    var k=0; // index of first value used by bucket
+    //for loop to validate and bucket the data
+    for (var i=1; i < glucoseData.length; ++i) {
+        var BGTime = glucoseData[i].date;
+        var lastBGTime = glucoseData[k].date;
+        var elapsedMinutes = (BGTime - lastBGTime)/(60*1000);
+
+        if(Math.abs(elapsedMinutes) >= 2) {
+            j++; // move to next bucket
+            k=i; // store index of first value used by bucket
+            bucketedData[j]=JSON.parse(JSON.stringify(glucoseData[i]));
+        } else {
+            // average all readings within time deadband
+            var glucoseTotal = glucoseData.slice(k, i+1).reduce(function(total, entry) {
+                return total + entry.glucose;
+            }, 0);
+            bucketedData[j].glucose = glucoseTotal / (i-k+1);
+        }
+    }
+    //console.error(bucketedData);
+    //console.error(bucketedData[bucketedData.length-1]);
+    // go through the treatments and remove any that are older than the oldest glucose value
+    //console.error(treatments);
+    for (i=treatments.length-1; i>0; --i) {
+        var treatment = treatments[i];
+        //console.error(treatment);
+        if (treatment) {
+            var treatmentDate = new Date(tz(treatment.timestamp));
+            var treatmentTime = treatmentDate.getTime();
+            var glucoseDatum = bucketedData[bucketedData.length-1];
+            //console.error(glucoseDatum);
+            if (glucoseDatum) {
+                var BGDate = new Date(glucoseDatum.date);
+                BGTime = BGDate.getTime();
+                if ( treatmentTime < BGTime ) {
+                    treatments.splice(i,1);
+                }
+            }
+        }
+    }
+    //console.error(treatments);
+    var calculatingCR = false;
+    var absorbing = 0;
+    var uam = 0; // unannounced meal
+    var mealCOB = 0;
+    var mealCarbs = 0;
+    var CRCarbs = 0;
+    var type="";
+    // main for loop
+    var fullHistory = IOBInputs.history;
+    for (i=bucketedData.length-5; i > 0; --i) {
+        glucoseDatum = bucketedData[i];
+        //console.error(glucoseDatum);
+        BGDate = new Date(glucoseDatum.date);
+        BGTime = BGDate.getTime();
+        // As we're processing each data point, go through the treatment.carbs and see if any of them are older than
+        // the current BG data point.  If so, add those carbs to COB.
+        treatment = treatments[treatments.length-1];
+        var myCarbs = 0;
+        if (treatment) {
+            treatmentDate = new Date(tz(treatment.timestamp));
+            treatmentTime = treatmentDate.getTime();
+            //console.error(treatmentDate);
+            if ( treatmentTime < BGTime ) {
+                if (treatment.carbs >= 1) {
+                    mealCOB += parseFloat(treatment.carbs);
+                    mealCarbs += parseFloat(treatment.carbs);
+                    myCarbs = treatment.carbs;
+                }
+                treatments.pop();
+            }
+        }
+
+        var BG;
+        var delta;
+        var avgDelta;
+        // TODO: re-implement interpolation to avoid issues here with gaps
+        // calculate avgDelta as last 4 datapoints to better catch more rises after COB hits zero
+        if (typeof(bucketedData[i].glucose) !== 'undefined' && typeof(bucketedData[i+4].glucose) !== 'undefined') {
+            //console.error(bucketedData[i]);
+            BG = bucketedData[i].glucose;
+            if ( BG < 40 || bucketedData[i+4].glucose < 40) {
+                //process.stderr.write("!");
+                continue;
+            }
+            delta = (BG - bucketedData[i+1].glucose);
+            avgDelta = (BG - bucketedData[i+4].glucose)/4;
+        } else { console.error("Could not find glucose data"); }
+
+        avgDelta = avgDelta.toFixed(2);
+        glucoseDatum.avgDelta = avgDelta;
+
+        //sens = ISF
+        var sens = ISF.isfLookup(IOBInputs.profile.isfProfile,BGDate);
+        IOBInputs.clock=BGDate.toISOString();
+        // trim down IOBInputs.history to just the data for 6h prior to BGDate
+        //console.error(IOBInputs.history[0].created_at);
+        var newHistory = [];
+        for (var h=0; h<fullHistory.length; h++) {
+            var hDate = new Date(fullHistory[h].created_at)
+            //console.error(fullHistory[i].created_at, hDate, BGDate, BGDate-hDate);
+            //if (h == 0 || h == fullHistory.length - 1) {
+                //console.error(hDate, BGDate, hDate-BGDate)
+            //}
+            if (BGDate-hDate < 6*60*60*1000 && BGDate-hDate > 0) {
+                //process.stderr.write("i");
+                //console.error(hDate);
+                newHistory.push(fullHistory[h]);
+            }
+        }
+        IOBInputs.history = newHistory;
+        // process.stderr.write("" + newHistory.length + " ");
+        //console.error(newHistory[0].created_at,newHistory[newHistory.length-1].created_at,newHistory.length);
+
+
+        // for IOB calculations, use the average of the last 4 hours' basals to help convergence;
+        // this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge.
+        // use the pumpbasalprofile to properly calculate IOB during periods where no temp basal is set
+        var currentPumpBasal = basal.basalLookup(opts.pumpbasalprofile, BGDate);
+        var BGDate1hAgo = new Date(BGTime-1*60*60*1000);
+        var BGDate2hAgo = new Date(BGTime-2*60*60*1000);
+        var BGDate3hAgo = new Date(BGTime-3*60*60*1000);
+        var basal1hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate1hAgo);
+        var basal2hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate2hAgo);
+        var basal3hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate3hAgo);
+        var sum = [currentPumpBasal,basal1hAgo,basal2hAgo,basal3hAgo].reduce(function(a, b) { return a + b; });
+        IOBInputs.profile.currentBasal = Math.round((sum/4)*1000)/1000;
+
+        // this is the current autotuned basal, used for everything else besides IOB calculations
+        var currentBasal = basal.basalLookup(opts.basalprofile, BGDate);
+
+        //console.error(currentBasal,basal1hAgo,basal2hAgo,basal3hAgo,IOBInputs.profile.currentBasal);
+        // basalBGI is BGI of basal insulin activity.
+        var basalBGI = Math.round(( currentBasal * sens / 60 * 5 )*100)/100; // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m
+        //console.log(JSON.stringify(IOBInputs.profile));
+        // call iob since calculated elsewhere
+        var iob = getIOB(IOBInputs)[0];
+        //console.error(JSON.stringify(iob));
+
+        // activity times ISF times 5 minutes is BGI
+        var BGI = Math.round(( -iob.activity * sens * 5 )*100)/100;
+        // datum = one glucose data point (being prepped to store in output)
+        glucoseDatum.BGI = BGI;
+        // calculating deviation
+        var deviation = avgDelta-BGI;
+        var dev5m = delta-BGI;
+        //console.error(deviation,avgDelta,BG,bucketedData[i].glucose);
+
+        // set positive deviations to zero if BG is below 80
+        if ( BG < 80 && deviation > 0 ) {
+            deviation = 0;
+        }
+
+        // rounding and storing deviation
+        deviation = deviation.toFixed(2);
+        dev5m = dev5m.toFixed(2);
+        glucoseDatum.deviation = deviation;
+
+
+        // Then, calculate carb absorption for that 5m interval using the deviation.
+        if ( mealCOB > 0 ) {
+            var profile = profileData;
+            var ci = Math.max(deviation, profile.min_5m_carbimpact);
+            var absorbed = ci * profile.carb_ratio / sens;
+            // Store the COB, and use it as the starting point for the next data point.
+            mealCOB = Math.max(0, mealCOB-absorbed);
+        }
+
+
+        // Calculate carb ratio (CR) independently of CSF and ISF
+        // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
+        // For now, if another meal IOB/COB stacks on top of it, consider them together
+        // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
+        // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
+
+        if (mealCOB > 0 || calculatingCR ) {
+            // set initial values when we first see COB
+            CRCarbs += myCarbs;
+            if (!calculatingCR) {
+                var CRInitialIOB = iob.iob;
+                var CRInitialBG = glucoseDatum.glucose;
+                var CRInitialCarbTime = new Date(glucoseDatum.date);
+                console.error("CRInitialIOB:",CRInitialIOB,"CRInitialBG:",CRInitialBG,"CRInitialCarbTime:",CRInitialCarbTime);
+            }
+            // keep calculatingCR as long as we have COB or enough IOB
+            if ( mealCOB > 0 && i>1 ) {
+                calculatingCR = true;
+            } else if ( iob.iob > currentBasal/2 && i>1 ) {
+                calculatingCR = true;
+            // when COB=0 and IOB drops low enough, record end values and be done calculatingCR
+            } else {
+                var CREndIOB = iob.iob;
+                var CREndBG = glucoseDatum.glucose;
+                var CREndTime = new Date(glucoseDatum.date);
+                console.error("CREndIOB:",CREndIOB,"CREndBG:",CREndBG,"CREndTime:",CREndTime);
+                var CRDatum = {
+                    CRInitialIOB: CRInitialIOB
+                ,   CRInitialBG: CRInitialBG
+                ,   CRInitialCarbTime: CRInitialCarbTime
+                ,   CREndIOB: CREndIOB
+                ,   CREndBG: CREndBG
+                ,   CREndTime: CREndTime
+                ,   CRCarbs: CRCarbs
+                };
+                //console.error(CRDatum);
+
+                var CRElapsedMinutes = Math.round((CREndTime - CRInitialCarbTime) / 1000 / 60);
+                //console.error(CREndTime - CRInitialCarbTime, CRElapsedMinutes);
+                if ( CRElapsedMinutes < 60 || ( i===1 && mealCOB > 0 ) ) {
+                    console.error("Ignoring",CRElapsedMinutes,"m CR period.");
+                } else {
+                    CRData.push(CRDatum);
+                }
+
+                CRCarbs = 0;
+                calculatingCR = false;
+            }
+        }
+
+
+        // If mealCOB is zero but all deviations since hitting COB=0 are positive, assign those data points to CSFGlucoseData
+        // Once deviations go negative for at least one data point after COB=0, we can use the rest of the data to tune ISF or basals
+        if (mealCOB > 0 || absorbing || mealCarbs > 0) {
+            // if meal IOB has decayed, then end absorption after this data point unless COB > 0
+            if ( iob.iob < currentBasal/2 ) {
+                absorbing = 0;
+            // otherwise, as long as deviations are positive, keep tracking carb deviations
+            } else if (deviation > 0) {
+                absorbing = 1;
+            } else {
+                absorbing = 0;
+            }
+            if ( ! absorbing && ! mealCOB ) {
+                mealCarbs = 0;
+            }
+            // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag
+            //console.error(type);
+            if ( type !== "csf" ) {
+                glucoseDatum.mealAbsorption = "start";
+                console.error(glucoseDatum.mealAbsorption,"carb absorption");
+            }
+            type="csf";
+            glucoseDatum.mealCarbs = mealCarbs;
+            //if (i == 0) { glucoseDatum.mealAbsorption = "end"; }
+            CSFGlucoseData.push(glucoseDatum);
+        } else {
+          // check previous "type" value, and if it was csf, set a mealAbsorption end flag
+          if ( type === "csf" ) {
+            CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end";
+            console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption");
+          }
+
+          if ((iob.iob > 2 * currentBasal || deviation > 6 || uam) ) {
+            if (deviation > 0) {
+                uam = 1;
+            } else {
+                uam = 0;
+            }
+            if ( type !== "uam" ) {
+                glucoseDatum.uamAbsorption = "start";
+                console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption");
+            }
+            type="uam";
+            UAMGlucoseData.push(glucoseDatum);
+          } else {
+            if ( type === "uam" ) {
+                console.error("end unannounced meal absorption");
+            }
+
+
+            // Go through the remaining time periods and divide them into periods where scheduled basal insulin activity dominates. This would be determined by calculating the BG impact of scheduled basal insulin (for example 1U/hr * 48 mg/dL/U ISF = 48 mg/dL/hr = 5 mg/dL/5m), and comparing that to BGI from bolus and net basal insulin activity.
+            // When BGI is positive (insulin activity is negative), we want to use that data to tune basals
+            // When BGI is smaller than about 1/4 of basalBGI, we want to use that data to tune basals
+            // When BGI is negative and more than about 1/4 of basalBGI, we can use that data to tune ISF,
+            // unless avgDelta is positive: then that's some sort of unexplained rise we don't want to use for ISF, so that means basals
+            if (basalBGI > -4 * BGI) {
+                type="basal";
+                basalGlucoseData.push(glucoseDatum);
+            } else {
+                if ( avgDelta > 0 && avgDelta > -2*BGI ) {
+                    //type="unknown"
+                    type="basal"
+                    basalGlucoseData.push(glucoseDatum);
+                } else {
+                    type="ISF";
+                    ISFGlucoseData.push(glucoseDatum);
+                }
+            }
+          }
+        }
+        // debug line to print out all the things
+        var BGDateArray = BGDate.toString().split(" ");
+        BGTime = BGDateArray[4];
+        // console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"basalBGI:",basalBGI.toFixed(1),"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",deviation,"avgDelta:",avgDelta,type);
+        console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",dev5m,"avgDev:",deviation,"avgDelta:",avgDelta,type,BG,myCarbs);
+    }
+
+    IOBInputs = {
+        profile: profileData
+    ,   history: opts.pumpHistory
+    };
+    treatments = find_insulin(IOBInputs);
+    CRData.forEach(function(CRDatum) {
+        var dosedOpts = {
+            treatments: treatments
+            , profile: opts.profile
+            , start: CRDatum.CRInitialCarbTime
+            , end: CRDatum.CREndTime
+        };
+        var insulinDosed = dosed(dosedOpts);
+        CRDatum.CRInsulin = insulinDosed.insulin;
+        //console.error(CRDatum);
+    });
+
+    var CSFLength = CSFGlucoseData.length;
+    var ISFLength = ISFGlucoseData.length;
+    var UAMLength = UAMGlucoseData.length;
+    var basalLength = basalGlucoseData.length;
+
+    if (opts.categorize_uam_as_basal) {
+        console.error("--categorize-uam-as-basal=true set: categorizing all UAM data as basal.");
+        basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData);
+    } else if (CSFLength > 12) {
+        console.error("Found at least 1h of carb absorption: assuming all meals were announced, and categorizing UAM data as basal.");
+        basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData);
+    } else {
+        if (2*basalLength < UAMLength) {
+            //console.error(basalGlucoseData, UAMGlucoseData);
+            console.error("Warning: too many deviations categorized as UnAnnounced Meals");
+            console.error("Adding",UAMLength,"UAM deviations to",basalLength,"basal ones");
+            basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData);
+            //console.error(basalGlucoseData);
+            // if too much data is excluded as UAM, add in the UAM deviations to basal, but then discard the highest 50%
+            basalGlucoseData.sort(function (a, b) {
+                return a.deviation - b.deviation;
+            });
+            var newBasalGlucose = basalGlucoseData.slice(0,basalGlucoseData.length/2);
+            //console.error(newBasalGlucose);
+            basalGlucoseData = newBasalGlucose;
+            console.error("and selecting the lowest 50%, leaving", basalGlucoseData.length, "basal+UAM ones");
+        }
+
+        if (2*ISFLength < UAMLength && ISFLength < 10) {
+            console.error("Adding",UAMLength,"UAM deviations to",ISFLength,"ISF ones");
+            ISFGlucoseData = ISFGlucoseData.concat(UAMGlucoseData);
+            // if too much data is excluded as UAM, add in the UAM deviations to ISF, but then discard the highest 50%
+            ISFGlucoseData.sort(function (a, b) {
+                return a.deviation - b.deviation;
+            });
+            var newISFGlucose = ISFGlucoseData.slice(0,ISFGlucoseData.length/2);
+            //console.error(newISFGlucose);
+            ISFGlucoseData = newISFGlucose;
+            console.error("and selecting the lowest 50%, leaving", ISFGlucoseData.length, "ISF+UAM ones");
+            //console.error(ISFGlucoseData.length, UAMLength);
+        }
+    }
+    basalLength = basalGlucoseData.length;
+    ISFLength = ISFGlucoseData.length;
+    if ( 4*basalLength + ISFLength < CSFLength && ISFLength < 10 ) {
+        console.error("Warning: too many deviations categorized as meals");
+        //console.error("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones");
+        //var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData);
+        console.error("Adding",CSFLength,"CSF deviations to",ISFLength,"ISF ones");
+        ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData);
+        CSFGlucoseData = [];
+    }
+
+
+    return {
+        CRData: CRData,
+        CSFGlucoseData: CSFGlucoseData,
+        ISFGlucoseData: ISFGlucoseData,
+        basalGlucoseData: basalGlucoseData
+    };
+}
+
+exports = module.exports = categorizeBGDatums;

+ 26 - 0
oref0/lib/lib/autotune-prep/dosed.js

@@ -0,0 +1,26 @@
+function insulinDosed(opts) {
+
+    var start = opts.start.getTime();
+    var end = opts.end.getTime();
+    var treatments = opts.treatments;
+    var profile_data = opts.profile;
+    var insulinDosed = 0;
+    if (!treatments) {
+        console.error("No treatments to process.");
+        return {};
+    }
+
+    treatments.forEach(function(treatment) {
+        //console.error(treatment);
+        if(treatment.insulin && treatment.date > start && treatment.date <= end) {
+            insulinDosed += treatment.insulin;
+        }
+    });
+    //console.error(insulinDosed);
+
+    return {
+        insulin: Math.round( insulinDosed * 1000 ) / 1000
+    };
+}
+
+exports = module.exports = insulinDosed;

+ 175 - 0
oref0/lib/lib/autotune-prep/index.js

@@ -0,0 +1,175 @@
+
+// Prep step before autotune.js can run; pulls in meal (carb) data and calls categorize.js 
+
+var find_meals = require('../meal/history');
+var categorize = require('./categorize');
+
+function generate (inputs) {
+
+  //console.error(inputs);
+  var treatments = find_meals(inputs);
+
+  var opts = {
+    treatments: treatments
+  , profile: inputs.profile
+  , pumpHistory: inputs.history
+  , glucose: inputs.glucose
+  //, prepped_glucose: inputs.prepped_glucose
+  , basalprofile: inputs.profile.basalprofile
+  , pumpbasalprofile: inputs.pumpprofile.basalprofile
+  , categorize_uam_as_basal: inputs.categorize_uam_as_basal
+  };
+
+  var autotune_prep_output = categorize(opts);
+
+  if (inputs.tune_insulin_curve) {
+    if (opts.profile.curve === 'bilinear') {
+      console.error('--tune-insulin-curve is set but only valid for exponential curves');
+    } else {
+      var minDeviations = 1000000;
+      var newDIA = 0;
+      var diaDeviations = [];
+      var peakDeviations = [];
+      var currentDIA = opts.profile.dia;
+      var currentPeak = opts.profile.insulinPeakTime;
+
+      var consoleError = console.error;
+      console.error = function() {};
+
+      var startDIA=currentDIA - 2;
+      var endDIA=currentDIA + 2;
+      for (var dia=startDIA; dia <= endDIA; ++dia) {
+        var sqrtDeviations = 0;
+        var deviations = 0;
+        var deviationsSq = 0;
+
+        opts.profile.dia = dia;
+
+        var curve_output = categorize(opts);
+        var basalGlucose = curve_output.basalGlucoseData;
+
+        for (var hour=0; hour < 24; ++hour) {
+          for (var i=0; i < basalGlucose.length; ++i) {
+            var BGTime;
+
+            if (basalGlucose[i].date) {
+              BGTime = new Date(basalGlucose[i].date);
+            } else if (basalGlucose[i].displayTime) {
+              BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
+            } else if (basalGlucose[i].dateString) {
+              BGTime = new Date(basalGlucose[i].dateString);
+            } else {
+              consoleError("Could not determine last BG time");
+            }
+
+            var myHour = BGTime.getHours();
+            if (hour === myHour) {
+              //console.error(basalGlucose[i].deviation);
+              sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5);
+              deviations += Math.abs(parseFloat(basalGlucose[i].deviation));
+              deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2);
+            }
+          }
+        }
+
+        var meanDeviation = Math.round(Math.abs(deviations/basalGlucose.length)*1000)/1000;
+        var SMRDeviation = Math.round(Math.pow(sqrtDeviations/basalGlucose.length,2)*1000)/1000;
+        var RMSDeviation = Math.round(Math.pow(deviationsSq/basalGlucose.length,0.5)*1000)/1000;
+        consoleError('insulinEndTime', dia, 'meanDeviation:', meanDeviation, 'SMRDeviation:', SMRDeviation, 'RMSDeviation:',RMSDeviation, '(mg/dL)');
+        diaDeviations.push({
+            dia: dia,
+            meanDeviation: meanDeviation,
+            SMRDeviation: SMRDeviation,
+            RMSDeviation: RMSDeviation,
+        });
+        autotune_prep_output.diaDeviations = diaDeviations;
+
+        deviations = Math.round(deviations*1000)/1000;
+        if (deviations < minDeviations) {
+          minDeviations = Math.round(deviations*1000)/1000;
+          newDIA = dia;
+        }
+      }
+
+      // consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)');
+      //consoleError(diaDeviations);
+
+      minDeviations = 1000000;
+
+      var newPeak = 0;
+      opts.profile.dia = currentDIA;
+      //consoleError(opts.profile.useCustomPeakTime, opts.profile.insulinPeakTime);
+      if ( ! opts.profile.useCustomPeakTime === true && opts.profile.curve === "ultra-rapid" ) {
+        opts.profile.insulinPeakTime = 55;
+      } else if ( ! opts.profile.useCustomPeakTime === true ) {
+        opts.profile.insulinPeakTime = 75;
+      }
+      opts.profile.useCustomPeakTime = true;
+
+      var startPeak=opts.profile.insulinPeakTime - 10;
+      var endPeak=opts.profile.insulinPeakTime + 10;
+      for (var peak=startPeak; peak <= endPeak; peak=(peak+5)) {
+        sqrtDeviations = 0;
+        deviations = 0;
+        deviationsSq = 0;
+
+        opts.profile.insulinPeakTime = peak;
+
+
+        curve_output = categorize(opts);
+        basalGlucose = curve_output.basalGlucoseData;
+
+        for (hour=0; hour < 24; ++hour) {
+          for (i=0; i < basalGlucose.length; ++i) {
+            if (basalGlucose[i].date) {
+              BGTime = new Date(basalGlucose[i].date);
+            } else if (basalGlucose[i].displayTime) {
+              BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
+            } else if (basalGlucose[i].dateString) {
+              BGTime = new Date(basalGlucose[i].dateString);
+            } else {
+              consoleError("Could not determine last BG time");
+            }
+
+            myHour = BGTime.getHours();
+            if (hour === myHour) {
+              //console.error(basalGlucose[i].deviation);
+              sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5);
+              deviations += Math.abs(parseFloat(basalGlucose[i].deviation));
+              deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2);
+            }
+          }
+        }
+        console.error(deviationsSq);
+
+        meanDeviation = Math.round(deviations/basalGlucose.length*1000)/1000;
+        SMRDeviation = Math.round(Math.pow(sqrtDeviations/basalGlucose.length,2)*1000)/1000;
+        RMSDeviation = Math.round(Math.pow(deviationsSq/basalGlucose.length,0.5)*1000)/1000;
+        consoleError('insulinPeakTime', peak, 'meanDeviation:', meanDeviation, 'SMRDeviation:', SMRDeviation, 'RMSDeviation:',RMSDeviation, '(mg/dL)');
+        peakDeviations.push({
+            peak: peak,
+            meanDeviation: meanDeviation,
+            SMRDeviation: SMRDeviation,
+            RMSDeviation: RMSDeviation,
+        });
+        autotune_prep_output.diaDeviations = diaDeviations;
+
+        deviations = Math.round(deviations*1000)/1000;
+        if (deviations < minDeviations) {
+          minDeviations = Math.round(deviations*1000)/1000;
+          newPeak = peak;
+        }
+      }
+
+      //consoleError('Optimum insulinPeakTime', newPeak, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)');
+      //consoleError(peakDeviations);
+      autotune_prep_output.peakDeviations = peakDeviations;
+
+      console.error = consoleError;
+    }
+  }
+
+  return autotune_prep_output;
+}
+
+exports = module.exports = generate;

+ 554 - 0
oref0/lib/lib/autotune/index.js

@@ -0,0 +1,554 @@
+var percentile = require('../percentile');
+//rounding of basals for pumps
+//20 for omnipod to 0.05
+//40 for medtronic to 0.025
+var basal_scale = 20;
+// does three things - tunes basals, ISF, and CSF
+
+function tuneAllTheThings (inputs) {
+
+    var previousAutotune = inputs.previousAutotune;
+    //console.error(previousAutotune);
+    var pumpProfile = inputs.pumpProfile;
+    var pumpBasalProfile = pumpProfile.basalprofile;
+    //console.error(pumpBasalProfile);
+    var basalProfile = previousAutotune.basalprofile;
+    //console.error(basalProfile);
+    var isfProfile = previousAutotune.isfProfile;
+    //console.error(isfProfile);
+    var ISF = isfProfile.sensitivities[0].sensitivity;
+    //console.error(ISF);
+    var carbRatio = previousAutotune.carb_ratio;
+    //console.error(carbRatio);
+    var CSF = ISF / carbRatio;
+    var DIA = previousAutotune.dia;
+    var peak = previousAutotune.insulinPeakTime;
+    if (! previousAutotune.useCustomPeakTime === true) {
+        if ( previousAutotune.curve === "ultra-rapid" ) {
+            peak = 55;
+        } else {
+            peak = 75;
+        }
+    }
+    //console.error(DIA, peak);
+
+    // conditional on there being a pump profile; if not then skip
+    if (pumpProfile) { var pumpISFProfile = pumpProfile.isfProfile; }
+    if (pumpISFProfile && pumpISFProfile.sensitivities[0]) {
+        var pumpISF = pumpISFProfile.sensitivities[0].sensitivity;
+        var pumpCarbRatio = pumpProfile.carb_ratio;
+        var pumpCSF = pumpISF / pumpCarbRatio;
+    }
+    if (! carbRatio) { carbRatio = pumpCarbRatio; }
+    if (! CSF) { CSF = pumpCSF; }
+    if (! ISF) { ISF = pumpISF; }
+    //console.error(CSF);
+    var preppedGlucose = inputs.preppedGlucose;
+    var CSFGlucose = preppedGlucose.CSFGlucoseData;
+    //console.error(CSFGlucose[0]);
+    var ISFGlucose = preppedGlucose.ISFGlucoseData;
+    //console.error(ISFGlucose[0]);
+    var basalGlucose = preppedGlucose.basalGlucoseData;
+    //console.error(basalGlucose[0]);
+    var CRData = preppedGlucose.CRData;
+    //console.error(CRData);
+    var diaDeviations = preppedGlucose.diaDeviations;
+    //console.error(diaDeviations);
+    var peakDeviations = preppedGlucose.peakDeviations;
+    //console.error(peakDeviations);
+
+    // tune DIA
+    var newDIA = DIA;
+    if (diaDeviations) {
+        var currentDIAMeanDev = diaDeviations[2].meanDeviation;
+        var currentDIARMSDev = diaDeviations[2].RMSDeviation;
+        //console.error(DIA,currentDIAMeanDev,currentDIARMSDev);
+        var minMeanDeviations = 1000000;
+        var minRMSDeviations = 1000000;
+        var meanBest = 2;
+        var RMSBest = 2;
+        for (var i=0; i < diaDeviations.length; i++) {
+            var meanDeviations = diaDeviations[i].meanDeviation;
+            var RMSDeviations = diaDeviations[i].RMSDeviation;
+            if (meanDeviations < minMeanDeviations) {
+                minMeanDeviations = Math.round(meanDeviations*1000)/1000;
+                meanBest = i;
+            }
+            if (RMSDeviations < minRMSDeviations) {
+                minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
+                RMSBest = i;
+            }
+        }
+        console.error("Best insulinEndTime for meanDeviations:",diaDeviations[meanBest].dia,"hours");
+        console.error("Best insulinEndTime for RMSDeviations:",diaDeviations[RMSBest].dia,"hours");
+        if ( meanBest < 2 && RMSBest < 2 ) {
+            if ( diaDeviations[1].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[1].RMSDeviation < currentDIARMSDev * 0.99 ) {
+                newDIA = diaDeviations[1].dia;
+            }
+        } else if ( meanBest > 2 && RMSBest > 2 ) {
+            if ( diaDeviations[3].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[3].RMSDeviation < currentDIARMSDev * 0.99 ) {
+                newDIA = diaDeviations[3].dia;
+            }
+        }
+        if ( newDIA > 12 ) {
+            console.error("insulinEndTime maximum is 12h: not raising further");
+            newDIA=12;
+        }
+        if ( newDIA !== DIA ) {
+            console.error("Adjusting insulinEndTime from",DIA,"to",newDIA,"hours");
+        } else {
+            console.error("Leaving insulinEndTime unchanged at",DIA,"hours");
+        }
+    }
+
+    // tune insulinPeakTime
+    var newPeak = peak;
+    if (peakDeviations && peakDeviations[2]) {
+        var currentPeakMeanDev = peakDeviations[2].meanDeviation;
+        var currentPeakRMSDev = peakDeviations[2].RMSDeviation;
+        //console.error(currentPeakMeanDev);
+        minMeanDeviations = 1000000;
+        minRMSDeviations = 1000000;
+        meanBest = 2;
+        RMSBest = 2;
+        for (i=0; i < peakDeviations.length; i++) {
+            meanDeviations = peakDeviations[i].meanDeviation;
+            RMSDeviations = peakDeviations[i].RMSDeviation;
+            if (meanDeviations < minMeanDeviations) {
+                minMeanDeviations = Math.round(meanDeviations*1000)/1000;
+                meanBest = i;
+            }
+            if (RMSDeviations < minRMSDeviations) {
+                minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
+                RMSBest = i;
+            }
+        }
+        console.error("Best insulinPeakTime for meanDeviations:",peakDeviations[meanBest].peak,"minutes");
+        console.error("Best insulinPeakTime for RMSDeviations:",peakDeviations[RMSBest].peak,"minutes");
+        if ( meanBest < 2 && RMSBest < 2 ) {
+            if ( peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[1].RMSDeviation < currentPeakRMSDev * 0.99 ) {
+                newPeak = peakDeviations[1].peak;
+            }
+        } else if ( meanBest > 2 && RMSBest > 2 ) {
+            if ( peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[3].RMSDeviation < currentPeakRMSDev * 0.99 ) {
+                newPeak = peakDeviations[3].peak;
+            }
+        }
+        if ( newPeak !== peak ) {
+            console.error("Adjusting insulinPeakTime from",peak,"to",newPeak,"minutes");
+        } else {
+            console.error("Leaving insulinPeakTime unchanged at",peak);
+        }
+    }
+
+
+
+    // Calculate carb ratio (CR) independently of CSF and ISF
+    // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
+    // For now, if another meal IOB/COB stacks on top of it, consider them together
+    // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
+    // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
+
+    var CRTotalCarbs = 0;
+    var CRTotalInsulin = 0;
+    CRData.forEach(function(CRDatum) {
+        var CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG;
+        var CRInsulinReq = CRBGChange / ISF;
+        var CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB;
+        CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq;
+        //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRDatum.CRInsulinTotal);
+        var CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000;
+        //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRDatum.CRInsulinTotal);
+        //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulin,"CRDatum.CRInsulinTotal:",CRDatum.CRInsulinTotal,"CR:",CR);
+        if (CRDatum.CRInsulinTotal > 0) {
+            CRTotalCarbs += CRDatum.CRCarbs;
+            CRTotalInsulin += CRDatum.CRInsulinTotal;
+            //console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin);
+        }
+    });
+    CRTotalInsulin = Math.round(CRTotalInsulin*1000)/1000;
+    var totalCR = Math.round( CRTotalCarbs / CRTotalInsulin * 1000 )/1000;
+    console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin,"totalCR:",totalCR);
+
+    // convert the basal profile to hourly if it isn't already
+    var hourlyBasalProfile = [];
+    var hourlyPumpProfile = [];
+    for (i=0; i < 24; i++) {
+        // autotuned basal profile
+        for (var j=0; j < basalProfile.length; ++j) {
+            if (basalProfile[j].minutes <= i * 60) {
+                if (basalProfile[j].rate === 0) {
+                    console.error("ERROR: bad basalProfile",basalProfile[j]);
+                    return;
+                }
+                hourlyBasalProfile[i] = JSON.parse(JSON.stringify(basalProfile[j]));
+            }
+        }
+        hourlyBasalProfile[i].i=i;
+        hourlyBasalProfile[i].minutes=i*60;
+        var zeroPadHour = ("000"+i).slice(-2);
+        hourlyBasalProfile[i].start=zeroPadHour + ":00:00";
+        hourlyBasalProfile[i].rate=Math.round(hourlyBasalProfile[i].rate*1000)/1000
+        // pump basal profile
+        if (pumpBasalProfile && pumpBasalProfile[0]) {
+            for (j=0; j < pumpBasalProfile.length; ++j) {
+                //console.error(pumpBasalProfile[j]);
+                if (pumpBasalProfile[j].rate === 0) {
+                    console.error("ERROR: bad pumpBasalProfile",pumpBasalProfile[j]);
+                    return;
+                }
+                if (pumpBasalProfile[j].minutes <= i * 60) {
+                    hourlyPumpProfile[i] = JSON.parse(JSON.stringify(pumpBasalProfile[j]));
+                }
+            }
+            hourlyPumpProfile[i].i=i;
+            hourlyPumpProfile[i].minutes=i*60;
+            hourlyPumpProfile[i].rate=Math.round(hourlyPumpProfile[i].rate*1000)/1000
+        }
+    }
+    //console.error(hourlyPumpProfile);
+    //console.error(hourlyBasalProfile);
+    var newHourlyBasalProfile = JSON.parse(JSON.stringify(hourlyBasalProfile));
+
+    // look at net deviations for each hour
+    for (var hour=0; hour < 24; hour++) {
+        var deviations = 0;
+        for (i=0; i < basalGlucose.length; ++i) {
+            var BGTime;
+
+            if (basalGlucose[i].date) {
+                BGTime = new Date(basalGlucose[i].date);
+            } else if (basalGlucose[i].displayTime) {
+                BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
+            } else if (basalGlucose[i].dateString) {
+                BGTime = new Date(basalGlucose[i].dateString);
+            } else {
+                console.error("Could not determine last BG time");
+            }
+
+            var myHour = BGTime.getHours();
+            if (hour === myHour) {
+                //console.error(basalGlucose[i].deviation);
+                deviations += parseFloat(basalGlucose[i].deviation);
+            }
+        }
+        deviations = Math.round( deviations * 1000 ) / 1000
+        console.error("Hour",hour.toString(),"total deviations:",deviations,"mg/dL");
+        // calculate how much less or additional basal insulin would have been required to eliminate the deviations
+        // only apply 20% of the needed adjustment to keep things relatively stable
+        var basalNeeded = 0.2 * deviations / ISF;
+        basalNeeded = Math.round( basalNeeded * 100 ) / 100
+        // if basalNeeded is positive, adjust each of the 1-3 hour prior basals by 10% of the needed adjustment
+        console.error("Hour",hour,"basal adjustment needed:",basalNeeded,"U/hr");
+        if (basalNeeded > 0 ) {
+            for (var offset=-3; offset < 0; offset++) {
+                var offsetHour = hour + offset;
+                if (offsetHour < 0) { offsetHour += 24; }
+                //console.error(offsetHour);
+                newHourlyBasalProfile[offsetHour].rate += basalNeeded / 3;
+                newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000
+            }
+        // otherwise, figure out the percentage reduction required to the 1-3 hour prior basals
+        // and adjust all of them downward proportionally
+        } else if (basalNeeded < 0) {
+            var threeHourBasal = 0;
+            for (offset=-3; offset < 0; offset++) {
+                offsetHour = hour + offset;
+                if (offsetHour < 0) { offsetHour += 24; }
+                threeHourBasal += newHourlyBasalProfile[offsetHour].rate;
+            }
+            var adjustmentRatio = 1.0 + basalNeeded / threeHourBasal;
+            //console.error(adjustmentRatio);
+            for (offset=-3; offset < 0; offset++) {
+                offsetHour = hour + offset;
+                if (offsetHour < 0) { offsetHour += 24; }
+                newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio;
+                newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000
+            }
+        }
+    }
+    if (pumpBasalProfile && pumpBasalProfile[0]) {
+        for (hour=0; hour < 24; hour++) {
+            //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2);
+            // cap adjustments at autosens_max and autosens_min
+            if (typeof pumpProfile.autosens_max !== 'undefined') {
+                var autotuneMax = pumpProfile.autosens_max;
+            } else {
+                var autotuneMax = 1.2;
+            }
+            if (typeof pumpProfile.autosens_min !== 'undefined') {
+                var autotuneMin = pumpProfile.autosens_min;
+            } else {
+                var autotuneMin = 0.7;
+            }
+            var maxRate = hourlyPumpProfile[hour].rate * autotuneMax;
+            var minRate = hourlyPumpProfile[hour].rate * autotuneMin;
+            if (newHourlyBasalProfile[hour].rate > maxRate ) {
+                console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is",autotuneMax,"* pump basal of",hourlyPumpProfile[hour].rate,")");
+                //console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is 20% above pump basal of",hourlyPumpProfile[hour].rate,")");
+                newHourlyBasalProfile[hour].rate = maxRate;
+            } else if (newHourlyBasalProfile[hour].rate < minRate ) {
+                console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is",autotuneMin,"* pump basal of",hourlyPumpProfile[hour].rate,")");
+                //console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is 20% below pump basal of",hourlyPumpProfile[hour].rate,")");
+                newHourlyBasalProfile[hour].rate = minRate;
+            }
+            newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate*basal_scale)/basal_scale;
+        }
+    }
+
+    // some hours of the day rarely have data to tune basals due to meals.
+    // when no adjustments are needed to a particular hour, we should adjust it toward the average of the
+    // periods before and after it that do have data to be tuned
+
+    var lastAdjustedHour = 0;
+    // scan through newHourlyBasalProfile and find hours where the rate is unchanged
+    for (hour=0; hour < 24; hour++) {
+        if (hourlyBasalProfile[hour].rate === newHourlyBasalProfile[hour].rate) {
+            var nextAdjustedHour = 23;
+            for (var nextHour = hour; nextHour < 24; nextHour++) {
+                if (! (hourlyBasalProfile[nextHour].rate === newHourlyBasalProfile[nextHour].rate)) {
+                    nextAdjustedHour = nextHour;
+                    break;
+                //} else {
+                    //console.error(nextHour, hourlyBasalProfile[nextHour].rate, newHourlyBasalProfile[nextHour].rate);
+                }
+            }
+            //console.error(hour, newHourlyBasalProfile);
+            newHourlyBasalProfile[hour].rate = Math.round( (0.8*hourlyBasalProfile[hour].rate + 0.1*newHourlyBasalProfile[lastAdjustedHour].rate + 0.1*newHourlyBasalProfile[nextAdjustedHour].rate)*basal_scale)/basal_scale;
+			if (newHourlyBasalProfile[hour].untuned)
+				newHourlyBasalProfile[hour].untuned++;
+			else
+				newHourlyBasalProfile[hour].untuned = 1;
+            console.error("Adjusting hour",hour,"basal from",hourlyBasalProfile[hour].rate,"to",newHourlyBasalProfile[hour].rate,"based on hour",lastAdjustedHour,"=",newHourlyBasalProfile[lastAdjustedHour].rate,"and hour",nextAdjustedHour,"=",newHourlyBasalProfile[nextAdjustedHour].rate);
+        } else {
+            lastAdjustedHour = hour;
+        }
+    }
+
+    console.error(newHourlyBasalProfile);
+    basalProfile = newHourlyBasalProfile;
+
+    // Calculate carb ratio (CR) independently of CSF and ISF
+    // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
+    // For now, if another meal IOB/COB stacks on top of it, consider them together
+    // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
+    // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
+
+
+
+    // calculate net deviations while carbs are absorbing
+    // measured from carb entry until COB and deviations both drop to zero
+
+    var deviations = 0;
+    var mealCarbs = 0;
+    var totalMealCarbs = 0;
+    var totalDeviations = 0;
+    var fullNewCSF;
+    //console.error(CSFGlucose[0].mealAbsorption);
+    //console.error(CSFGlucose[0]);
+    for (i=0; i < CSFGlucose.length; ++i) {
+        //console.error(CSFGlucose[i].mealAbsorption, i);
+        if ( CSFGlucose[i].mealAbsorption === "start" ) {
+            deviations = 0;
+            mealCarbs = parseInt(CSFGlucose[i].mealCarbs);
+        } else if (CSFGlucose[i].mealAbsorption === "end") {
+            deviations += parseFloat(CSFGlucose[i].deviation);
+            // compare the sum of deviations from start to end vs. current CSF * mealCarbs
+            //console.error(CSF,mealCarbs);
+            var csfRise = CSF * mealCarbs;
+            //console.error(deviations,ISF);
+            //console.error("csfRise:",csfRise,"deviations:",deviations);
+            totalMealCarbs += mealCarbs;
+            totalDeviations += deviations;
+
+        } else {
+            deviations += Math.max(0*previousAutotune.min_5m_carbimpact,parseFloat(CSFGlucose[i].deviation));
+            mealCarbs = Math.max(mealCarbs, parseInt(CSFGlucose[i].mealCarbs));
+        }
+    }
+    // at midnight, write down the mealcarbs as total meal carbs (to prevent special case of when only one meal and it not finishing absorbing by midnight)
+    // TODO: figure out what to do with dinner carbs that don't finish absorbing by midnight
+    if (totalMealCarbs === 0) { totalMealCarbs += mealCarbs; }
+    if (totalDeviations === 0) { totalDeviations += deviations; }
+    //console.error(totalDeviations, totalMealCarbs);
+    if (totalMealCarbs === 0) {
+        // if no meals today, CSF is unchanged
+        fullNewCSF = CSF;
+    } else {
+        // how much change would be required to account for all of the deviations
+        fullNewCSF = Math.round( (totalDeviations / totalMealCarbs)*100 )/100;
+    }
+    // only adjust by 20%
+    var newCSF = ( 0.8 * CSF ) + ( 0.2 * fullNewCSF );
+    // safety cap CSF
+    if (typeof(pumpCSF) !== 'undefined') {
+        var maxCSF = pumpCSF * autotuneMax;
+        var minCSF = pumpCSF * autotuneMin;
+        if (newCSF > maxCSF) {
+            console.error("Limiting CSF to",maxCSF.toFixed(2),"(which is",autotuneMax,"* pump CSF of",pumpCSF,")");
+            newCSF = maxCSF;
+        } else if (newCSF < minCSF) {
+            console.error("Limiting CSF to",minCSF.toFixed(2),"(which is",autotuneMin,"* pump CSF of",pumpCSF,")");
+            newCSF = minCSF;
+        } //else { console.error("newCSF",newCSF,"is close enough to",pumpCSF); }
+    }
+    var oldCSF = Math.round( CSF * 1000 ) / 1000;
+    newCSF = Math.round( newCSF * 1000 ) / 1000;
+    totalDeviations = Math.round ( totalDeviations * 1000 )/1000;
+    console.error("totalMealCarbs:",totalMealCarbs,"totalDeviations:",totalDeviations,"oldCSF",oldCSF,"fullNewCSF:",fullNewCSF,"newCSF:",newCSF);
+    // this is where CSF is set based on the outputs
+    if (newCSF) {
+        CSF = newCSF;
+    }
+
+    if (totalCR === 0) {
+        // if no meals today, CR is unchanged
+        var fullNewCR = carbRatio;
+    } else {
+        // how much change would be required to account for all of the deviations
+        fullNewCR = totalCR;
+    }
+    // don't tune CR out of bounds
+    var maxCR = pumpCarbRatio * autotuneMax;
+    if (maxCR > 150) { maxCR = 150 }
+    var minCR = pumpCarbRatio * autotuneMin;
+    if (minCR < 1) { minCR = 3 }
+    // safety cap fullNewCR
+    if (typeof(pumpCarbRatio) !== 'undefined') {
+        if (fullNewCR > maxCR) {
+            console.error("Limiting fullNewCR from",fullNewCR,"to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")");
+            fullNewCR = maxCR;
+        } else if (fullNewCR < minCR) {
+            console.error("Limiting fullNewCR from",fullNewCR,"to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")");
+            fullNewCR = minCR;
+        } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); }
+    }
+    // only adjust by 20%
+    var newCR = ( 0.8 * carbRatio ) + ( 0.2 * fullNewCR );
+    // safety cap newCR
+    if (typeof(pumpCarbRatio) !== 'undefined') {
+        if (newCR > maxCR) {
+            console.error("Limiting CR to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")");
+            newCR = maxCR;
+        } else if (newCR < minCR) {
+            console.error("Limiting CR to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")");
+            newCR = minCR;
+        } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); }
+    }
+    newCR = Math.round( newCR * 1000 ) / 1000;
+    console.error("oldCR:",carbRatio,"fullNewCR:",fullNewCR,"newCR:",newCR);
+    // this is where CR is set based on the outputs
+    //var ISFFromCRAndCSF = ISF;
+    if (newCR) {
+        carbRatio = newCR;
+        //ISFFromCRAndCSF = Math.round( carbRatio * CSF * 1000)/1000;
+    }
+
+
+
+    // calculate median deviation and bgi in data attributable to ISF
+    var deviations = [];
+    var BGIs = [];
+    var avgDeltas = [];
+    var ratios = [];
+    for (i=0; i < ISFGlucose.length; ++i) {
+        deviation = parseFloat(ISFGlucose[i].deviation);
+        deviations.push(deviation);
+        var BGI = parseFloat(ISFGlucose[i].BGI);
+        BGIs.push(BGI);
+        var avgDelta = parseFloat(ISFGlucose[i].avgDelta);
+        avgDeltas.push(avgDelta);
+        var ratio = 1 + deviation / BGI;
+        //console.error("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio);
+        ratios.push(ratio);
+    }
+    avgDeltas.sort(function(a, b){return a-b});
+    BGIs.sort(function(a, b){return a-b});
+    deviations.sort(function(a, b){return a-b});
+    ratios.sort(function(a, b){return a-b});
+    var p50deviation = percentile(deviations, 0.50);
+    var p50BGI = percentile(BGIs, 0.50);
+    var p50ratios = Math.round( percentile(ratios, 0.50) * 1000)/1000;
+    var fullNewISF = ISF;
+    if (ISFGlucose.length < 10) {
+        // leave ISF unchanged if fewer than 10 ISF data points
+        console.error ("Only found",ISFGlucose.length,"ISF data points, leaving ISF unchanged at",ISF);
+    } else {
+        // calculate what adjustments to ISF would have been necessary to bring median deviation to zero
+        fullNewISF = ISF * p50ratios;
+    }
+    fullNewISF = Math.round( fullNewISF * 1000 ) / 1000;
+    //console.error("p50ratios:",p50ratios,"fullNewISF:",fullNewISF,ratios[Math.floor(ratios.length/2)]);
+    // adjust the target ISF to be a weighted average of fullNewISF and pumpISF
+    var adjustmentFraction;
+
+    if (typeof(pumpProfile.autotune_isf_adjustmentFraction) !== 'undefined') {
+        adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction;
+    } else {
+        adjustmentFraction = 1.0;
+    }
+
+    // low autosens ratio = high ISF
+    var maxISF = pumpISF / autotuneMin;
+    // high autosens ratio = low ISF
+    var minISF = pumpISF / autotuneMax;
+    if (typeof(pumpISF) !== 'undefined') {
+        if ( fullNewISF < 0 ) {
+            var adjustedISF = ISF;
+        } else {
+            adjustedISF = adjustmentFraction*fullNewISF + (1-adjustmentFraction)*pumpISF;
+        }
+        // cap adjustedISF before applying 10%
+        //console.error(adjustedISF, maxISF, minISF);
+        if (adjustedISF > maxISF) {
+            console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")");
+            adjustedISF = maxISF;
+        } else if (adjustedISF < minISF) {
+            console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")");
+            adjustedISF = minISF;
+        }
+
+        // and apply 20% of that adjustment
+        var newISF = ( 0.8 * ISF ) + ( 0.2 * adjustedISF );
+
+        if (newISF > maxISF) {
+            console.error("Limiting ISF of",newISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")");
+            newISF = maxISF;
+        } else if (newISF < minISF) {
+            console.error("Limiting ISF of",newISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")");
+            newISF = minISF;
+        }
+    }
+    newISF = Math.round( newISF * 1000 ) / 1000;
+    //console.error(avgRatio);
+    //console.error(newISF);
+    p50deviation = Math.round( p50deviation * 1000 ) / 1000;
+    p50BGI = Math.round( p50BGI * 1000 ) / 1000;
+    adjustedISF = Math.round( adjustedISF * 1000 ) / 1000;
+    console.error("p50deviation:",p50deviation,"p50BGI",p50BGI,"p50ratios:",p50ratios,"Old ISF:",ISF,"fullNewISF:",fullNewISF,"adjustedISF:",adjustedISF,"newISF:",newISF,"newDIA:",newDIA,"newPeak:",newPeak);
+
+    if (newISF) {
+        ISF = newISF;
+    }
+
+
+    // reconstruct updated version of previousAutotune as autotuneOutput
+    var autotuneOutput = previousAutotune;
+    autotuneOutput.basalprofile = basalProfile;
+    isfProfile.sensitivities[0].sensitivity = ISF;
+    autotuneOutput.isfProfile = isfProfile;
+    autotuneOutput.sens = ISF;
+    autotuneOutput.csf = CSF;
+    //carbRatio = ISF / CSF;
+    carbRatio = Math.round( carbRatio * 1000 ) / 1000;
+    autotuneOutput.carb_ratio = carbRatio;
+    autotuneOutput.dia = newDIA;
+    autotuneOutput.insulinPeakTime = newPeak;
+    if (diaDeviations || peakDeviations) {
+        autotuneOutput.useCustomPeakTime = true;
+    }
+
+    return autotuneOutput;
+}
+
+exports = module.exports = tuneAllTheThings;

+ 60 - 0
oref0/lib/lib/basal-set-temp.js

@@ -0,0 +1,60 @@
+'use strict';
+
+function reason(rT, msg) {
+  rT.reason = (rT.reason ? rT.reason + '. ' : '') + msg;
+  console.error(msg);
+}
+
+var tempBasalFunctions = {};
+
+tempBasalFunctions.getMaxSafeBasal = function getMaxSafeBasal(profile) {
+
+    var max_daily_safety_multiplier = (isNaN(profile.max_daily_safety_multiplier) || profile.max_daily_safety_multiplier === null) ? 3 : profile.max_daily_safety_multiplier;
+    var current_basal_safety_multiplier = (isNaN(profile.current_basal_safety_multiplier) || profile.current_basal_safety_multiplier === null) ? 4 : profile.current_basal_safety_multiplier;
+
+    return Math.min(profile.max_basal, max_daily_safety_multiplier * profile.max_daily_basal, current_basal_safety_multiplier * profile.current_basal);
+};
+
+tempBasalFunctions.setTempBasal = function setTempBasal(rate, duration, profile, rT, currenttemp) {
+    //var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal);
+
+    var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile);
+    var round_basal = require('./round-basal');
+
+    if (rate < 0) {
+        rate = 0;
+    } else if (rate > maxSafeBasal) {
+        rate = maxSafeBasal;
+    }
+
+    var suggestedRate = round_basal(rate, profile);
+    if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && typeof(currenttemp.rate) !== 'undefined' && currenttemp.duration > (duration-10) && currenttemp.duration <= 120 && suggestedRate <= currenttemp.rate * 1.2 && suggestedRate >= currenttemp.rate * 0.8 && duration > 0 ) {
+        rT.reason += " "+currenttemp.duration+"m left and " + currenttemp.rate + " ~ req " + suggestedRate + "U/hr: no temp required";
+        return rT;
+    }
+
+    if (suggestedRate === profile.current_basal) {
+      if (profile.skip_neutral_temps === true) {
+        if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && currenttemp.duration > 0) {
+          reason(rT, 'Suggested rate is same as profile rate, a temp basal is active, canceling current temp');
+          rT.duration = 0;
+          rT.rate = 0;
+          return rT;
+        } else {
+          reason(rT, 'Suggested rate is same as profile rate, no temp basal is active, doing nothing');
+          return rT;
+        }
+      } else {
+        reason(rT, 'Setting neutral temp basal of ' + profile.current_basal + 'U/hr');
+        rT.duration = duration;
+        rT.rate = suggestedRate;
+        return rT;
+      }
+    } else {
+      rT.duration = duration;
+      rT.rate = suggestedRate;
+      return rT;
+    }
+};
+
+module.exports = tempBasalFunctions;

+ 180 - 0
oref0/lib/lib/bolus.js

@@ -0,0 +1,180 @@
+
+function reduce (treatments) {
+
+  var results = [ ];
+
+  var state = { };
+  var previous = [ ];
+
+  function in_previous (ev) {
+    var found = false;
+    previous.forEach(function (elem) {
+      if (elem.timestamp === ev.timestamp && ev._type === elem._type) {
+        found = true;
+      }
+    });
+
+    return found;
+  }
+
+  function within_minutes_from (origin, tail, minutes) {
+    var ms = minutes * 1000 * 60;
+    var ts = Date.parse(origin.timestamp);
+    return /* candidates */ tail.slice( ).filter(function (elem) {
+      var dt = Date.parse(elem.timestamp);
+      return ts - dt <= ms;
+    });
+  }
+
+  function bolus (ev, remaining) {
+    if (!ev) { console.error('XXX', ev, remaining); return; }
+    if (ev._type === 'BolusWizard') {
+      state.carbs = ev.carb_input.toString( );
+      state.ratio = ev.carb_ratio.toString( );
+      if (ev.bg) {
+        state.bg = ev.bg.toString( );
+        state.glucose = ev.bg.toString( );
+        state.glucoseType = ev._type;
+      }
+      state.wizard = ev;
+      state.created_at = state.timestamp = ev.timestamp;
+      previous.push(ev);
+    }
+
+    if (ev._type === 'Bolus') {
+      state.duration = ev.duration.toString( );
+      // if (state.square || state.bolus) { }
+      // state.insulin = (state.insulin ? state.insulin : 0) + ev.amount;
+      if (ev.duration && ev.duration > 0) {
+        state.square = ev;
+      } else {
+        if (state.bolus) {
+          state.bolus.amount = state.bolus.amount + ev.amount;
+        } else
+          state.bolus = ev;
+      }
+      state.created_at = state.timestamp = ev.timestamp;
+      previous.push(ev);
+    }
+
+    if (remaining && remaining.length > 0) {
+      if (state.bolus && state.wizard) {
+        // skip to end
+        return bolus({}, []);
+      }
+      // keep recursing
+      return bolus(remaining[0], remaining.slice(1));
+    } else {
+      // console.error("state", state);
+      // console.error("remaining", remaining);
+      state.eventType = '<none>';
+
+      state.insulin = (state.insulin ? state.insulin : 0) + (state.square ? state.square.amount : 0) +
+            (state.bolus ? state.bolus.amount : 0);
+      var has_insulin = state.insulin && state.insulin > 0;
+      var has_carbs = state.carbs && state.carbs > 0;
+      if (state.square && state.bolus) {
+        annotate("DualWave bolus for", state.square.duration, "minutes");
+      } else if (state.square && state.wizard) {
+        annotate("Square wave bolus for", state.square.duration, "minutes");
+      } else if (state.square) {
+        annotate("Solo Square wave bolus for", state.square.duration, "minutes");
+        annotate("No bolus wizard used.");
+      } else if (state.bolus && state.wizard) {
+        annotate("Normal bolus with wizard.");
+      } else if (state.bolus) {
+        annotate("Normal bolus (solo, no bolus wizard).");
+      }
+
+      if (has_insulin) {
+        var iobFile = "./monitor/iob.json";
+        var fs = require('fs');
+        if (fs.existsSync(iobFile)) {
+            var iob = JSON.parse(fs.readFileSync(iobFile));
+            if (iob && Array.isArray(iob) && iob.length) {
+                annotate("Calculated IOB:", iob[0].iob);
+            }
+        }
+      }
+
+      if (state.bolus) {
+        annotate("Programmed bolus", state.bolus.programmed);
+        annotate("Delivered bolus", state.bolus.amount);
+        annotate("Percent delivered: ", (state.bolus.amount/state.bolus.programmed * 100).toString( ) + '%');
+      }
+      if (state.square) {
+        annotate("Programmed square", state.square.programmed);
+        annotate("Delivered square", state.square.amount);
+        annotate("Success: ", (state.square.amount/state.square.programmed * 100).toString( ) + '%');
+      }
+      if (state.wizard) {
+        state.created_at = state.wizard.timestamp;
+        annotate("Food estimate", state.wizard.food_estimate);
+        annotate("Correction estimate", state.wizard.correction_estimate);
+        annotate("Bolus estimate", state.wizard.bolus_estimate);
+        annotate("Target low", state.wizard.bg_target_low);
+        annotate("Target high", state.wizard.bg_target_high);
+        var delta = state.wizard.sensitivity * state.insulin * -1;
+        annotate("Hypothetical glucose delta", delta);
+        if (state.bg && state.bg > 0) {
+          annotate('Glucose was:', state.bg);
+          // state.glucose = state.bg;
+          // TODO: annotate prediction
+        }
+      }
+      if (has_carbs && has_insulin) {
+        state.eventType = 'Meal Bolus';
+      } else {
+        if (has_carbs && !has_insulin) {
+          state.eventType = 'Carb Correction';
+        }
+        if (!has_carbs && has_insulin) {
+          state.eventType = 'Correction Bolus';
+        }
+      }
+      if (state.notes && state.notes.length > 0) {
+        state.notes = state.notes.join("\n");
+      }
+      if (state.insulin) {
+        state.insulin = state.insulin.toString( );
+      }
+
+      results.push(state);
+      state = { };
+    }
+  }
+
+  function annotate (msg) {
+    var args = [ ].slice.apply(arguments);
+    msg = args.join(' ');
+    if (!state.notes) {
+      state.notes = [ ];
+    }
+    state.notes.push(msg);
+  }
+
+  function step (current, index) {
+    if (in_previous(current)) {
+      return;
+    }
+    switch (current._type) {
+      case 'Bolus':
+      case 'BolusWizard':
+        var tail = within_minutes_from(current, treatments.slice(index+1), 2);
+        bolus(current, tail);
+        break;
+      case 'JournalEntryMealMarker':
+        current.carbs = current.carb_input;
+        current.eventType = 'Carb Correction';
+        results.push(current);
+        break;
+      default:
+        results.push(current);
+        break;
+    }
+  }
+  treatments.forEach(step);
+  return results;
+}
+
+exports = module.exports = reduce;

+ 30 - 0
oref0/lib/lib/calc-glucose-stats.js

@@ -0,0 +1,30 @@
+const moment = require('moment');
+const _ = require('lodash');
+const stats = require('./glucose-stats');
+
+module.exports = {};
+const calcStatsExports = module.exports;
+
+calcStatsExports.updateGlucoseStats = (options) => {
+  var hist = _.map(_.sortBy(options.glucose_hist, 'dateString'), function readDate(value) {
+      value.readDateMills = moment(value.dateString).valueOf();
+      return value;
+    });
+
+  if (hist && hist.length > 0) {
+    var noise_val = stats.calcSensorNoise(null, hist, null, null);
+
+    var ns_noise_val = stats.calcNSNoise(noise_val, hist);
+
+    if ('noise' in options.glucose_hist[0]) {
+      console.error("Glucose noise CGM reported level: ", options.glucose_hist[0].noise);
+      ns_noise_val = Math.max(ns_noise_val, options.glucose_hist[0].noise);
+    }
+
+    console.error("Glucose noise calculated: ", noise_val, " setting noise level to ", ns_noise_val);
+
+    options.glucose_hist[0].noise = ns_noise_val;
+  }
+
+  return options.glucose_hist;
+};

+ 451 - 0
oref0/lib/lib/determine-basal/autosens.js

@@ -0,0 +1,451 @@
+var basal = require('../profile/basal');
+var get_iob = require('../iob');
+var find_insulin = require('../iob/history');
+var isf = require('../profile/isf');
+var find_meals = require('../meal/history');
+var tz = require('moment-timezone');
+var percentile = require('../percentile');
+
+function detectSensitivity(inputs) {
+
+    //console.error(inputs.glucose_data[0]);
+    var glucose_data = inputs.glucose_data.map(function prepGlucose (obj) {
+        //Support the NS sgv field to avoid having to convert in a custom way
+        obj.glucose = obj.glucose || obj.sgv;
+        return obj;
+    });
+    //console.error(glucose_data[0]);
+    var iob_inputs = inputs.iob_inputs;
+    var basalprofile = inputs.basalprofile;
+    var profile = inputs.iob_inputs.profile;
+
+    // use last 24h worth of data by default
+    if (inputs.retrospective) {
+        //console.error(glucose_data[0]);
+        var lastSiteChange = new Date(new Date(glucose_data[0].date).getTime() - (24 * 60 * 60 * 1000));
+    } else {
+        lastSiteChange = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
+    }
+    if (inputs.iob_inputs.profile.rewind_resets_autosens === true ) {
+        // scan through pumphistory and set lastSiteChange to the time of the last pump rewind event
+        // if not present, leave lastSiteChange unchanged at 24h ago.
+        var history = inputs.iob_inputs.history;
+        for (var h=1; h < history.length; ++h) {
+            if ( ! history[h]._type || history[h]._type !== "Rewind" ) {
+                //process.stderr.write("-");
+                continue;
+            }
+            if ( history[h].timestamp ) {
+                lastSiteChange = new Date( history[h].timestamp );
+                console.error("Setting lastSiteChange to",lastSiteChange,"using timestamp",history[h].timestamp);
+                break;
+            }
+        }
+    }
+
+    // get treatments from pumphistory once, not every time we get_iob()
+    var treatments = find_insulin(inputs.iob_inputs);
+
+    var mealinputs = {
+        history: inputs.iob_inputs.history
+    , profile: profile
+    , carbs: inputs.carbs
+    , glucose: inputs.glucose_data
+    //, prepped_glucose: prepped_glucose_data
+    };
+    var meals = find_meals(mealinputs);
+    meals.sort(function (a, b) {
+        var aDate = new Date(tz(a.timestamp));
+        var bDate = new Date(tz(b.timestamp));
+        //console.error(aDate);
+        return bDate.getTime() - aDate.getTime();
+    });
+    //console.error(meals);
+
+    var avgDeltas = [];
+    var bgis = [];
+    var deviations = [];
+    var deviationSum = 0;
+    var bucketed_data = [];
+    glucose_data.reverse();
+    bucketed_data[0] = glucose_data[0];
+    //console.error(bucketed_data[0]);
+    var j=0;
+    // go through the meal treatments and remove any that are older than the oldest glucose value
+    //console.error(meals);
+    for (var i=1; i < glucose_data.length; ++i) {
+        var bgTime;
+        var lastbgTime;
+        if (glucose_data[i].display_time) {
+            bgTime = new Date(glucose_data[i].display_time.replace('T', ' '));
+        } else if (glucose_data[i].dateString) {
+            bgTime = new Date(glucose_data[i].dateString);
+        } else if (glucose_data[i].xDrip_started_at) {
+            continue;
+        } else { console.error("Could not determine BG time"); }
+        if (glucose_data[i-1].display_time) {
+            lastbgTime = new Date(glucose_data[i-1].display_time.replace('T', ' '));
+        } else if (glucose_data[i-1].dateString) {
+            lastbgTime = new Date(glucose_data[i-1].dateString);
+        } else if (bucketed_data[0].display_time) {
+            lastbgTime = new Date(bucketed_data[0].display_time.replace('T', ' '));
+        } else if (glucose_data[i-1].xDrip_started_at) {
+            continue;
+        } else { console.error("Could not determine last BG time"); }
+        if (glucose_data[i].glucose < 39 || glucose_data[i-1].glucose < 39) {
+//console.error("skipping:",glucose_data[i].glucose,glucose_data[i-1].glucose);
+            continue;
+        }
+        // only consider BGs since lastSiteChange
+        if (lastSiteChange) {
+            var hoursSinceSiteChange = (bgTime-lastSiteChange)/(60*60*1000);
+            if (hoursSinceSiteChange < 0) {
+                //console.error(hoursSinceSiteChange, bgTime, lastSiteChange);
+                continue;
+            }
+        }
+        var elapsed_minutes = (bgTime - lastbgTime)/(60*1000);
+        if(Math.abs(elapsed_minutes) > 2) {
+            j++;
+            bucketed_data[j]=glucose_data[i];
+            bucketed_data[j].date = bgTime.getTime();
+            //console.error(elapsed_minutes, bucketed_data[j].glucose, glucose_data[i].glucose);
+        } else {
+            bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose)/2;
+            //console.error(bucketed_data[j].glucose, glucose_data[i].glucose);
+        }
+    }
+    bucketed_data.shift();
+    //console.error(bucketed_data[0]);
+    for (i=meals.length-1; i>0; --i) {
+        var treatment = meals[i];
+        //console.error(treatment);
+        if (treatment) {
+            var treatmentDate = new Date(tz(treatment.timestamp));
+            var treatmentTime = treatmentDate.getTime();
+            var glucoseDatum = bucketed_data[0];
+            //console.error(glucoseDatum);
+            if (! glucoseDatum || ! glucoseDatum.date) {
+              //console.error("No date found on: ",glucoseDatum);
+              continue;
+            }
+            var BGDate = new Date(glucoseDatum.date);
+            var BGTime = BGDate.getTime();
+            if ( treatmentTime < BGTime ) {
+                //console.error("Removing old meal: ",treatmentDate);
+                meals.splice(i,1);
+            }
+        }
+    }
+    var absorbing = 0;
+    var uam = 0; // unannounced meal
+    var mealCOB = 0;
+    var mealCarbs = 0;
+    var mealStartCounter = 999;
+    var type="";
+    //console.error(bucketed_data);
+    for (i=3; i < bucketed_data.length; ++i) {
+        bgTime = new Date(bucketed_data[i].date);
+
+        var sens = isf.isfLookup(profile.isfProfile,bgTime);
+
+        //console.error(bgTime , bucketed_data[i].glucose);
+        var bg;
+        var avgDelta;
+        var delta;
+        if (typeof(bucketed_data[i].glucose) !== 'undefined') {
+            bg = bucketed_data[i].glucose;
+            var last_bg = bucketed_data[i-1].glucose;
+            var old_bg = bucketed_data[i-3].glucose;
+            if ( isNaN(bg) || !bg || bg < 40 || isNaN(old_bg) || !old_bg || old_bg < 40 || isNaN(last_bg) || !last_bg || last_bg < 40) {
+                process.stderr.write("!");
+                continue;
+            }
+            avgDelta = (bg - old_bg)/3;
+            delta = (bg - last_bg);
+        } else {
+            console.error("Could not find glucose data");
+            continue;
+        }
+
+        avgDelta = avgDelta.toFixed(2);
+        iob_inputs.clock=bgTime;
+        iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime);
+        // make sure autosens doesn't use temptarget-adjusted insulin calculations
+        iob_inputs.profile.temptargetSet = false;
+        //console.log(JSON.stringify(iob_inputs.profile));
+        //console.error("Before: ", new Date().getTime());
+        var iob = get_iob(iob_inputs, true, treatments)[0];
+        //console.error("After: ", new Date().getTime());
+        //console.log(JSON.stringify(iob));
+
+        var bgi = Math.round(( -iob.activity * sens * 5 )*100)/100;
+        bgi = bgi.toFixed(2);
+        //console.error(delta);
+        var deviation;
+        if (isNaN(delta) ) {
+            console.error("Bad delta: ",delta, bg, last_bg, old_bg);
+        } else {
+            deviation = delta-bgi;
+        }
+        //if (!deviation) { console.error(deviation, delta, bgi); }
+        // set positive deviations to zero if BG is below 80
+        if ( bg < 80 && deviation > 0 ) {
+            deviation = 0;
+        }
+        deviation = deviation.toFixed(2);
+
+        glucoseDatum = bucketed_data[i];
+        //console.error(glucoseDatum);
+        BGDate = new Date(glucoseDatum.date);
+        BGTime = BGDate.getTime();
+        // As we're processing each data point, go through the treatment.carbs and see if any of them are older than
+        // the current BG data point.  If so, add those carbs to COB.
+        treatment = meals[meals.length-1];
+        if (treatment) {
+            treatmentDate = new Date(tz(treatment.timestamp));
+            treatmentTime = treatmentDate.getTime();
+            if ( treatmentTime < BGTime ) {
+                if (treatment.carbs >= 1) {
+            //console.error(treatmentDate, treatmentTime, BGTime, BGTime-treatmentTime);
+                    mealCOB += parseFloat(treatment.carbs);
+                    mealCarbs += parseFloat(treatment.carbs);
+                    var displayCOB = Math.round(mealCOB);
+                    //console.error(displayCOB, mealCOB, treatment.carbs);
+                    process.stderr.write(displayCOB.toString()+"g");
+                }
+                meals.pop();
+            }
+        }
+
+        // calculate carb absorption for that 5m interval using the deviation.
+        if ( mealCOB > 0 ) {
+            //var profile = profileData;
+            var ci = Math.max(deviation, profile.min_5m_carbimpact);
+            var absorbed = ci * profile.carb_ratio / sens;
+            if (absorbed) {
+                mealCOB = Math.max(0, mealCOB-absorbed);
+            } else {
+                console.error(absorbed, ci, profile.carb_ratio, sens, deviation, profile.min_5m_carbimpact);
+            }
+        }
+
+        // If mealCOB is zero but all deviations since hitting COB=0 are positive, exclude from autosens
+        //console.error(mealCOB, absorbing, mealCarbs);
+        if (mealCOB > 0 || absorbing || mealCarbs > 0) {
+            if (deviation > 0 ) {
+                absorbing = 1;
+            } else {
+                absorbing = 0;
+            }
+            // stop excluding positive deviations as soon as mealCOB=0 if meal has been absorbing for >5h
+            if ( mealStartCounter > 60 && mealCOB < 0.5 ) {
+                displayCOB = Math.round(mealCOB);
+                process.stderr.write(displayCOB.toString()+"g");
+                absorbing = 0;
+            }
+            if ( ! absorbing && mealCOB < 0.5 ) {
+                mealCarbs = 0;
+            }
+            // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag
+            //console.error(type);
+            if ( type !== "csf" ) {
+                process.stderr.write("(");
+                mealStartCounter = 0;
+                //glucoseDatum.mealAbsorption = "start";
+                //console.error(glucoseDatum.mealAbsorption,"carb absorption");
+            }
+            mealStartCounter++;
+            type="csf";
+            glucoseDatum.mealCarbs = mealCarbs;
+            //if (i == 0) { glucoseDatum.mealAbsorption = "end"; }
+            //CSFGlucoseData.push(glucoseDatum);
+        } else {
+          // check previous "type" value, and if it was csf, set a mealAbsorption end flag
+          if ( type === "csf" ) {
+            process.stderr.write(")");
+            //CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end";
+            //console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption");
+          }
+
+          var currentBasal = iob_inputs.profile.current_basal;
+          // always exclude the first 45m after each carb entry using mealStartCounter
+          //if (iob.iob > currentBasal || uam ) {
+          if ((!inputs.retrospective && iob.iob > 2 * currentBasal) || uam || mealStartCounter < 9 ) {
+            mealStartCounter++;
+            if (deviation > 0) {
+                uam = 1;
+            } else {
+                uam = 0;
+            }
+            if ( type !== "uam" ) {
+                process.stderr.write("u(");
+                //glucoseDatum.uamAbsorption = "start";
+                //console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption");
+            }
+            //console.error(mealStartCounter);
+            type="uam";
+          } else {
+            if ( type === "uam" ) {
+                process.stderr.write(")");
+                //console.error("end unannounced meal absorption");
+            }
+            type = "non-meal"
+          }
+        }
+
+        // Exclude meal-related deviations (carb absorption) from autosens
+        if ( type === "non-meal" ) {
+            if ( deviation > 0 ) {
+                //process.stderr.write(" "+bg.toString());
+                process.stderr.write("+");
+            } else if ( deviation === 0 ) {
+                process.stderr.write("=");
+            } else {
+                //process.stderr.write(" "+bg.toString());
+                process.stderr.write("-");
+            }
+            avgDeltas.push(avgDelta);
+            bgis.push(bgi);
+            deviations.push(deviation);
+            deviationSum += parseFloat(deviation);
+        } else {
+            process.stderr.write("x");
+        }
+        // add an extra negative deviation if a high temptarget is running and exercise mode is set
+        if (profile.high_temptarget_raises_sensitivity === true || profile.exercise_mode === true) {
+            var tempTarget = tempTargetRunning(inputs.temptargets, bgTime)
+            if (tempTarget) {
+                //console.error(tempTarget)
+            }
+            if ( tempTarget > 100 ) {
+                // for a 110 temptarget, add a -0.5 deviation, for 160 add -3
+                var tempDeviation=-(tempTarget-100)/20;
+                process.stderr.write("-");
+                //console.error(tempDeviation)
+                deviations.push(tempDeviation);
+            }
+        }
+
+        var minutes = bgTime.getMinutes();
+        var hours = bgTime.getHours();
+        if ( minutes >= 0 && minutes < 5 ) {
+            //console.error(bgTime);
+            process.stderr.write(hours.toString()+"h");
+            // add one neutral deviation every 2 hours to help decay over long exclusion periods
+            if ( hours % 2 === 0 ) {
+                deviations.push(0);
+                process.stderr.write("=");
+            }
+        }
+        var lookback = inputs.deviations;
+        if (!lookback) { lookback = 96; }
+        // only keep the last 96 non-excluded data points (8h+ for any exclusions)
+        if (deviations.length > lookback) {
+            deviations.shift();
+        }
+    }
+    //console.error("");
+    process.stderr.write(" ");
+    //console.log(JSON.stringify(avgDeltas));
+    //console.log(JSON.stringify(bgis));
+    // when we have less than 8h worth of deviation data, add up to 90m of zero deviations
+    // this dampens any large sensitivity changes detected based on too little data, without ignoring them completely
+    console.error("");
+    console.error("Using most recent",deviations.length,"deviations since",lastSiteChange);
+    if (deviations.length < 96) {
+        var pad = Math.round((1 - deviations.length/96) * 18);
+        console.error("Adding",pad,"more zero deviations");
+        for (var d=0; d<pad; d++) {
+            //process.stderr.write(".");
+            deviations.push(0);
+        }
+    }
+    avgDeltas.sort(function(a, b){return a-b});
+    bgis.sort(function(a, b){return a-b});
+    deviations.sort(function(a, b){return a-b});
+    for (i=0.9; i > 0.1; i = i - 0.01) {
+        //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2));
+        if ( percentile(deviations, (i+0.01)) >= 0 && percentile(deviations, i) < 0 ) {
+            //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2));
+            var lessThanZero = Math.round(100*i);
+            console.error(lessThanZero+"% of non-meal deviations negative (>50% = sensitivity)");
+        }
+        if ( percentile(deviations, (i+0.01)) > 0 && percentile(deviations, i) <= 0 ) {
+            //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2));
+            var greaterThanZero = 100-Math.round(100*i);
+            console.error(greaterThanZero+"% of non-meal deviations positive (>50% = resistance)");
+        }
+    }
+    var pSensitive = percentile(deviations, 0.50);
+    var pResistant = percentile(deviations, 0.50);
+
+    var average = deviationSum / deviations.length;
+    //console.error("Mean deviation: "+average.toFixed(2));
+    
+    var squareDeviations = deviations.reduce(function(acc, dev){var dev_f = parseFloat(dev); return acc + dev_f * dev_f}, 0);
+    var rmsDev = Math.sqrt(squareDeviations / deviations.length);
+    console.error("RMS deviation: "+rmsDev.toFixed(2)); 
+
+    var basalOff = 0;
+
+    if(pSensitive < 0) { // sensitive
+        basalOff = pSensitive * (60/5) / profile.sens;
+        process.stderr.write("Insulin sensitivity detected: ");
+    } else if (pResistant > 0) { // resistant
+        basalOff = pResistant * (60/5) / profile.sens;
+        process.stderr.write("Insulin resistance detected: ");
+    } else {
+        console.error("Sensitivity normal.");
+    }
+    ratio = 1 + (basalOff / profile.max_daily_basal);
+    //console.error(basalOff, profile.max_daily_basal, ratio);
+
+    // don't adjust more than 1.2x by default (set in preferences.json)
+    var rawRatio = ratio;
+    ratio = Math.max(ratio, profile.autosens_min);
+    ratio = Math.min(ratio, profile.autosens_max);
+
+    if (ratio !== rawRatio) {
+      console.error('Ratio limited from ' + rawRatio + ' to ' + ratio);
+    }
+
+    ratio = Math.round(ratio*100)/100;
+    newisf = Math.round(profile.sens / ratio);
+    //console.error(profile, newisf, ratio);
+    console.error("ISF adjusted from "+profile.sens+" to "+newisf);
+    //console.error("Basal adjustment "+basalOff.toFixed(2)+"U/hr");
+    //console.error("Ratio: "+ratio*100+"%: new ISF: "+newisf.toFixed(1)+"mg/dL/U");
+    return {
+        "ratio": ratio,
+        "newisf": newisf
+    }
+}
+module.exports = detectSensitivity;
+
+function tempTargetRunning(temptargets_data, time) {
+    // sort tempTargets by date so we can process most recent first
+    try {
+        temptargets_data.sort(function (a, b) { return new Date(a.created_at) < new Date(b.created_at) });
+    } catch (e) {
+        //console.error("Could not sort temptargets_data.  Optional feature temporary targets disabled.");
+    }
+    //console.error(temptargets_data);
+    //console.error(time);
+    for (var i = 0; i < temptargets_data.length; i++) {
+        var start = new Date(temptargets_data[i].created_at);
+        //console.error(start);
+        var expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000);
+        //console.error(expires);
+        if (time >= new Date(temptargets_data[i].created_at) && temptargets_data[i].duration === 0) {
+            // cancel temp targets
+            //console.error(temptargets_data[i]);
+            return 0;
+        } else if (time >= new Date(temptargets_data[i].created_at) && time < expires ) {
+            //console.error(temptargets_data[i]);
+            var tempTarget = ( temptargets_data[i].targetTop + temptargets_data[i].targetBottom ) / 2;
+            //console.error(tempTarget);
+            return tempTarget;
+        }
+    }
+}

+ 205 - 0
oref0/lib/lib/determine-basal/cob.js

@@ -0,0 +1,205 @@
+var basal = require('../profile/basal');
+var get_iob = require('../iob');
+var find_insulin = require('../iob/history');
+var isf = require('../profile/isf');
+
+function detectCarbAbsorption(inputs) {
+
+    var glucose_data = inputs.glucose_data.map(function prepGlucose (obj) {
+        //Support the NS sgv field to avoid having to convert in a custom way
+        obj.glucose = obj.glucose || obj.sgv;
+        return obj;
+    });
+    var iob_inputs = inputs.iob_inputs;
+    var basalprofile = inputs.basalprofile;
+    /* TODO why does declaring profile break tests-command-behavior.tests.sh? */ profile = inputs.iob_inputs.profile;
+    var mealTime = new Date(inputs.mealTime);
+    var ciTime = new Date(inputs.ciTime);
+
+    //console.error(mealTime, ciTime);
+
+    // get treatments from pumphistory once, not every time we get_iob()
+    var treatments = find_insulin(inputs.iob_inputs);
+
+    var avgDeltas = [];
+    var bgis = [];
+    var deviations = [];
+    var deviationSum = 0;
+    var carbsAbsorbed = 0;
+    var bucketed_data = [];
+    bucketed_data[0] = glucose_data[0];
+    var j=0;
+    var foundPreMealBG = false;
+    var lastbgi = 0;
+
+    if (! glucose_data[0].glucose || glucose_data[0].glucose < 39) {
+      lastbgi = -1;
+    }
+
+    for (var i=1; i < glucose_data.length; ++i) {
+        var bgTime;
+        var lastbgTime;
+        if (glucose_data[i].display_time) {
+            bgTime = new Date(glucose_data[i].display_time.replace('T', ' '));
+        } else if (glucose_data[i].dateString) {
+            bgTime = new Date(glucose_data[i].dateString);
+        } else { console.error("Could not determine BG time"); }
+        if (! glucose_data[i].glucose || glucose_data[i].glucose < 39) {
+//console.error("skipping:",glucose_data[i].glucose);
+            continue;
+        }
+        // only consider BGs for 6h after a meal for calculating COB
+        var hoursAfterMeal = (bgTime-mealTime)/(60*60*1000);
+        if (hoursAfterMeal > 6 || foundPreMealBG) {
+            continue;
+        } else if (hoursAfterMeal < 0) {
+//console.error("Found pre-meal BG:",glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100);
+            foundPreMealBG = true;
+        }
+//console.error(glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100, bucketed_data[bucketed_data.length-1].display_time);
+        // only consider last ~45m of data in CI mode
+        // this allows us to calculate deviations for the last ~30m
+        if (typeof ciTime !== 'undefined') {
+            var hoursAgo = (ciTime-bgTime)/(45*60*1000);
+            if (hoursAgo > 1 || hoursAgo < 0) {
+                continue;
+            }
+        }
+        if (bucketed_data[bucketed_data.length-1].display_time) {
+            lastbgTime = new Date(bucketed_data[bucketed_data.length-1].display_time.replace('T', ' '));
+        } else if ((lastbgi >= 0) && glucose_data[lastbgi].display_time) {
+            lastbgTime = new Date(glucose_data[lastbgi].display_time.replace('T', ' '));
+        } else if ((lastbgi >= 0) && glucose_data[lastbgi].dateString) {
+            lastbgTime = new Date(glucose_data[lastbgi].dateString);
+        } else { console.error("Could not determine last BG time"); }
+        var elapsed_minutes = (bgTime - lastbgTime)/(60*1000);
+    //console.error(bgTime, lastbgTime, elapsed_minutes);
+        if(Math.abs(elapsed_minutes) > 8) {
+            // interpolate missing data points
+            var lastbg = glucose_data[lastbgi].glucose;
+            // cap interpolation at a maximum of 4h
+            elapsed_minutes = Math.min(240,Math.abs(elapsed_minutes));
+            //console.error(elapsed_minutes);
+            while(elapsed_minutes > 5) {
+                var previousbgTime = new Date(lastbgTime.getTime() - 5 * 60*1000);
+                j++;
+                bucketed_data[j] = [];
+                bucketed_data[j].date = previousbgTime.getTime();
+                var gapDelta = glucose_data[i].glucose - lastbg;
+                //console.error(gapDelta, lastbg, elapsed_minutes);
+                var previousbg = lastbg + (5/elapsed_minutes * gapDelta);
+                bucketed_data[j].glucose = Math.round(previousbg);
+                //console.error("Interpolated", bucketed_data[j]);
+
+                elapsed_minutes = elapsed_minutes - 5;
+                lastbg = previousbg;
+                lastbgTime = new Date(previousbgTime);
+            }
+
+        } else if(Math.abs(elapsed_minutes) > 2) {
+            j++;
+            bucketed_data[j]=glucose_data[i];
+            bucketed_data[j].date = bgTime.getTime();
+        } else {
+            bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose)/2;
+        }
+
+        lastbgi = i;
+        //console.error(bucketed_data[j].date)
+    }
+    var currentDeviation;
+    var slopeFromMaxDeviation = 0;
+    var slopeFromMinDeviation = 999;
+    var maxDeviation = 0;
+    var minDeviation = 999;
+    var allDeviations = [];
+    //console.error(bucketed_data);
+    for (i=0; i < bucketed_data.length-3; ++i) {
+        bgTime = new Date(bucketed_data[i].date);
+
+        var sens = isf.isfLookup(profile.isfProfile,bgTime);
+
+        //console.error(bgTime , bucketed_data[i].glucose, bucketed_data[i].date);
+        var bg;
+        var avgDelta;
+        var delta;
+        if (typeof(bucketed_data[i].glucose) !== 'undefined') {
+            bg = bucketed_data[i].glucose;
+            if ( bg < 39 || bucketed_data[i+3].glucose < 39) {
+                process.stderr.write("!");
+                continue;
+            }
+            avgDelta = (bg - bucketed_data[i+3].glucose)/3;
+            delta = (bg - bucketed_data[i+1].glucose);
+        } else { console.error("Could not find glucose data"); }
+
+        avgDelta = avgDelta.toFixed(2);
+        iob_inputs.clock=bgTime;
+        iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime);
+        //console.log(JSON.stringify(iob_inputs.profile));
+        //console.error("Before: ", new Date().getTime());
+        var iob = get_iob(iob_inputs, true, treatments)[0];
+        //console.error("After: ", new Date().getTime());
+        //console.error(JSON.stringify(iob));
+
+        var bgi = Math.round(( -iob.activity * sens * 5 )*100)/100;
+        bgi = bgi.toFixed(2);
+        //console.error(delta);
+        var deviation = delta-bgi;
+        deviation = deviation.toFixed(2);
+        //if (deviation < 0 && deviation > -2) { console.error("BG: "+bg+", avgDelta: "+avgDelta+", BGI: "+bgi+", deviation: "+deviation); }
+        // calculate the deviation right now, for use in min_5m
+        if (i===0) {
+            currentDeviation = Math.round((avgDelta-bgi)*1000)/1000;
+            if (ciTime > bgTime) {
+                //console.error("currentDeviation:",currentDeviation,avgDelta,bgi);
+                allDeviations.push(Math.round(currentDeviation));
+            }
+            if (currentDeviation/2 > profile.min_5m_carbimpact) {
+                //console.error("currentDeviation",currentDeviation,"/2 > min_5m_carbimpact",profile.min_5m_carbimpact);
+            }
+        } else if (ciTime > bgTime) {
+            var avgDeviation = Math.round((avgDelta-bgi)*1000)/1000;
+            var deviationSlope = (avgDeviation-currentDeviation)/(bgTime-ciTime)*1000*60*5;
+            //console.error(avgDeviation,currentDeviation,bgTime,ciTime)
+            if (avgDeviation > maxDeviation) {
+                slopeFromMaxDeviation = Math.min(0, deviationSlope);
+                maxDeviation = avgDeviation;
+            }
+            if (avgDeviation < minDeviation) {
+                slopeFromMinDeviation = Math.max(0, deviationSlope);
+                minDeviation = avgDeviation;
+            }
+
+            //console.error("Deviations:",avgDeviation, avgDelta,bgi,bgTime);
+            allDeviations.push(Math.round(avgDeviation));
+            //console.error(allDeviations);
+        }
+
+        // if bgTime is more recent than mealTime
+        if(bgTime > mealTime) {
+            // figure out how many carbs that represents
+            // if currentDeviation is > 2 * min_5m_carbimpact, assume currentDeviation/2 worth of carbs were absorbed
+            // but always assume at least profile.min_5m_carbimpact (3mg/dL/5m by default) absorption
+            var ci = Math.max(deviation, currentDeviation/2, profile.min_5m_carbimpact);
+            var absorbed = ci * profile.carb_ratio / sens;
+            // and add that to the running total carbsAbsorbed
+            //console.error("carbsAbsorbed:",carbsAbsorbed,"absorbed:",absorbed,"bgTime:",bgTime,"BG:",bucketed_data[i].glucose)
+            carbsAbsorbed += absorbed;
+        }
+    }
+    if(maxDeviation>0) {
+        //console.error("currentDeviation:",currentDeviation,"maxDeviation:",maxDeviation,"slopeFromMaxDeviation:",slopeFromMaxDeviation);
+    }
+
+    return {
+        "carbsAbsorbed": carbsAbsorbed
+    ,   "currentDeviation": currentDeviation
+    ,   "maxDeviation": maxDeviation
+    ,   "minDeviation": minDeviation
+    ,   "slopeFromMaxDeviation": slopeFromMaxDeviation
+    ,   "slopeFromMinDeviation": slopeFromMinDeviation
+    ,   "allDeviations": allDeviations
+    }
+}
+module.exports = detectCarbAbsorption;

File diff suppressed because it is too large
+ 2053 - 0
oref0/lib/lib/determine-basal/determine-basal.js


+ 329 - 0
oref0/lib/lib/glucose-get-last.js

@@ -0,0 +1,329 @@
+function getDateFromEntry(entry) {
+return entry.date || Date.parse(entry.display_time) || Date.parse(entry.dateString);
+}
+
+function round(value, digits)
+{
+    if (! digits) { digits = 0; }
+    var scale = Math.pow(10, digits);
+    return Math.round(value * scale) / scale;
+}
+
+var getLastGlucose = function (data) {
+    data = data.filter(function(obj) {
+    return obj.glucose || obj.sgv;
+    }).map(function prepGlucose (obj) {
+        //Support the NS sgv field to avoid having to convert in a custom way
+        obj.glucose = obj.glucose || obj.sgv;
+        if ( obj.glucose !== null ) {
+            return obj;
+        }
+    });
+
+    var now = data[0];
+    var now_date = getDateFromEntry(now);
+    var change;
+    var last_deltas = [];
+    var short_deltas = [];
+    var long_deltas = [];
+    var last_cal = 0;
+
+    //console.error(now.glucose);
+    for (var i=1; i < data.length; i++) {
+        // if we come across a cal record, don't process any older SGVs
+        if (typeof data[i] !== 'undefined' && data[i].type === "cal") {
+            last_cal = i;
+            break;
+        }
+        // only use data from the same device as the most recent BG data point
+        if (typeof data[i] !== 'undefined' && data[i].glucose > 38 && data[i].device === now.device) {
+            var then = data[i];
+            var then_date = getDateFromEntry(then);
+            var avgdelta = 0;
+            var minutesago;
+            if (typeof then_date !== 'undefined' && typeof now_date !== 'undefined') {
+                minutesago = Math.round( (now_date - then_date) / (1000 * 60) );
+                // multiply by 5 to get the same units as delta, i.e. mg/dL/5m
+                change = now.glucose - then.glucose;
+                avgdelta = change/minutesago * 5;
+            } else { console.error("Error: date field not found: cannot calculate avgdelta"); }
+            //if (i < 5) {
+                //console.error(then.glucose, minutesago, avgdelta);
+            //}
+            // use the average of all data points in the last 2.5m for all further "now" calculations
+            if (-2 < minutesago && minutesago < 2.5) {
+                now.glucose = ( now.glucose + then.glucose ) / 2;
+                now_date = ( now_date + then_date ) / 2;
+                //console.error(then.glucose, now.glucose);
+            // short_deltas are calculated from everything ~5-15 minutes ago
+            } else if (2.5 < minutesago && minutesago < 17.5) {
+                //console.error(minutesago, avgdelta);
+                short_deltas.push(avgdelta);
+                // last_deltas are calculated from everything ~5 minutes ago
+                if (2.5 < minutesago && minutesago < 7.5) {
+                    last_deltas.push(avgdelta);
+                }
+                //console.error(then.glucose, minutesago, avgdelta, last_deltas, short_deltas);
+            // long_deltas are calculated from everything ~20-40 minutes ago
+            } else if (17.5 < minutesago && minutesago < 42.5) {
+                long_deltas.push(avgdelta);
+            }
+        }
+    }
+    var last_delta = 0;
+    var short_avgdelta = 0;
+    var long_avgdelta = 0;
+
+    // start autoISF by https://github.com/ga-zelle/autoISF , relevant variables and functions
+    // mod 7: append 2 variables for 5% range
+    var autoISF_duration = 0;
+    var autoISF_average = 0;
+    // mod 8: append 3 variables for deltas based on regression analysis
+    var slope05 = 0;
+    var slope15 = 0;
+    var slope40 = 0;
+    // mod 14f: append results from best fitting parabola
+    var dura_p = 0;
+    var delta_pl = 0;
+    var delta_pn = 0;
+    var r_squ = 0;
+    var bg_acceleration = 0;
+    var a_0 = 0;
+    var a_1 = 0;
+    var a_2 = 0;
+    var pp_debug = "autoISF Mod14-Debug: ";
+
+    if (last_deltas.length > 0) {
+        last_delta = last_deltas.reduce(function(a, b) { return a + b; }) / last_deltas.length;
+    }
+    if (short_deltas.length > 0) {
+        short_avgdelta = short_deltas.reduce(function(a, b) { return a + b; }) / short_deltas.length;
+    }
+    if (long_deltas.length > 0) {
+        long_avgdelta = long_deltas.reduce(function(a, b) { return a + b; }) / long_deltas.length;
+    }
+    var bw = 0.05;
+    var sumBG = now.glucose;
+    var oldavg = now.glucose;
+    var minutesdur = 0;
+    for (var i = 1; i < data.length; i++) {
+        var then = data[i];
+        var then_date = getDateFromEntry(then);
+    //  mod 7c: stop the series if there was a CGM gap greater than 13 minutes, i.e. 2 regular readings
+            if (Math.round((now_date - then_date) / (1000 * 60)) - minutesdur > 13) {
+            break;
+            }
+            if (then.glucose > oldavg*(1-bw) && then.glucose < oldavg*(1+bw)) {
+            sumBG += then.glucose;
+            oldavg = sumBG / (i+1);
+            minutesdur = Math.round((now_date - then_date) / (1000 * 60));
+            } else {
+            break;
+        }
+    }
+            autoISF_average = oldavg;
+            autoISF_duration = minutesdur;
+
+            // mod 8: calculate 3 variables for deltas based on linear regression
+            // initially just test the handling of arguments
+            var slope05 = 1.05;
+            var slope15 = 1.15;
+            var slope40 = 1.40;
+
+            // mod 8a: now do the real maths based on
+            // http://www.carl-engler-schule.de/culm/culm/culm2/th_messdaten/mdv2/auszug_ausgleichsgerade.pdf
+            var sumBG  = 0;         // y
+            var sumt   = 0;         // x
+            var sumBG2 = 0;         // y^2
+            var sumt2  = 0;         // x^2
+            var sumxy  = 0;         // x*y
+            //double a;
+            var b;                   // y = a + b * x
+            var level = 7.5;
+            var minutesL;
+            // here, longer deltas include all values from 0 up the related limit
+            for (var i = 0; i < data.length; i++) {
+                var then = data[i];
+                var then_date = getDateFromEntry(then);
+                minutesL = (now_date - then_date) / (1000 * 60);
+                // watch out: the scan goes backwards in time, so delta has wrong sign
+                if(i * sumt2 == sumt * sumt) {
+                    b = 0.0;
+                }
+                else {
+                    b = (i * sumxy - sumt * sumBG) / (i * sumt2 - sumt * sumt);
+                }
+                if (minutesL > level && level == 7.5) {
+                    slope05 = -b * 5;
+                    level = 17.5;
+                }
+                if (minutesL > level && level == 17.5) {
+                    slope15 = -b * 5;
+                    level = 42.5;
+                }
+                if (minutesL > level && level == 42.5) {
+                    slope40 = -b * 5;
+                    break;
+                }
+
+                sumt   += minutesL;
+                sumt2  += minutesL * minutesL;
+                sumBG  += then.glucose;
+                sumBG2 += then.glucose * then.glucose;
+                sumxy  += then.glucose * minutesL;
+            }
+
+            // mod 14f: calculate best parabola and determine delta by extending it 5 minutes into the future
+            // nach https://www.codeproject.com/Articles/63170/Least-Squares-Regression-for-Quadratic-Curve-Fitti
+            //
+            //  y = a2*x^2 + a1*x + a0      or
+            //  y = a*x^2  + b*x  + c       respectively
+
+            // initially just test the handling of arguments
+            var dura_p  = 0;
+            var delta_pl = 0;
+            var delta_pn = 0;
+            var bg_acceleration = 0;
+            var r_squ   = 0;
+            var best_a = 0;
+            var best_b = 0;
+            var best_c = 0;
+            var a_0 = 0;
+            var a_1 = 0;
+            var a_2 = 0;
+
+            if (data.length <= 3) {                      // last 3 points make a trivial parabola
+                dura_p  = 0;
+                delta_pl = 0;
+                delta_pn = 0;
+                bg_acceleration = 0;
+                r_squ   = 0;
+                a_0 = 0;
+                a_1 = 0;
+                a_2 = 0;
+            } else {
+                //double corrMin = 0.90;                  // go backwards until the correlation coefficient goes below
+                var sy    = 0;                        // y
+                var sx    = 0;                        // x
+                var sx2   = 0;                        // x^2
+                var sx3   = 0;                        // x^3
+                var sx4   = 0;                        // x^4
+                var sxy   = 0;                        // x*y
+                var sx2y  = 0;                        // x^2*y
+                var corrMax = 0;
+                var iframe = data[0];
+                var time_0 = getDateFromEntry(iframe);
+                var ti_last = 0;
+                //# for best numerical accurarcy time and bg must be of same order of magnitude
+                var scaleTime = 300;                  //# in 5m; values are  0, 1, 2, 3, 4, ...
+                var scaleBg   =  50;                  //# TIR range is now 1.4 - 3.6
+
+                for (var i = 0; i < data.length; i++) {
+                    var then = data[i];
+                    var then_date = getDateFromEntry(then);
+                    // skip records older than 47.5 minutes
+                    var ti = (then_date - time_0) / 1000 / scaleTime;
+                    if (-ti *scaleTime > 47 * 60) {                        // skip records older than 47.5 minutes
+                        break;
+                    } else if (ti < ti_last - 7.5 * 60 / scaleTime) {       // stop scan if a CGM gap > 7.5 minutes is detected
+                        if ( i<3) {                             // history too short for fit
+                            dura_p =  -ti_last / 60;
+                            delta_pl = 0;
+                            delta_pn = 0;
+                            bg_acceleration= 0;
+                            r_squ = 0;
+                            a_0 = 0;
+                            a_1 = 0;
+                            a_2 = 0;
+                        }
+                        break;
+                    }
+                    ti_last = ti;
+                    var bg = then.glucose/scaleBg;
+                    sx += ti;
+                    sx2 += Math.pow(ti, 2);
+                    sx3 += Math.pow(ti, 3);
+                    sx4 += Math.pow(ti, 4);
+                    sy  += bg;
+                    sxy += ti * bg;
+                    sx2y += Math.pow(ti, 2) * bg;
+                    var n = i + 1;
+                    var D  = 0;
+                    var Da = 0;
+                    var Db = 0;
+                    var Dc = 0;
+                    if (n > 3) {
+                        D  = sx4 * (sx2 * n - sx * sx) - sx3 * (sx3 * n - sx * sx2) + sx2 * (sx3 * sx - sx2 * sx2);
+                        Da = sx2y* (sx2 * n - sx * sx) - sxy * (sx3 * n - sx * sx2) + sy  * (sx3 * sx - sx2 * sx2);
+                        Db = sx4 * (sxy * n - sy * sx) - sx3 * (sx2y* n - sy * sx2) + sx2 * (sx2y* sx - sxy * sx2);
+                        Dc = sx4 * (sx2 *sy - sx *sxy) - sx3 * (sx3 *sy - sx *sx2y) + sx2 * (sx3 *sxy - sx2 * sx2y);
+                    }
+                    if (D != 0) {
+                        var a = Da / D;
+                        b = Db / D;              // b initialised in linear fit !
+                        var c = Dc / D;
+                        var y_mean = sy / n;
+                        var s_squares = 0;
+                        var s_residual_squares = 0;
+                        for (var j = 0; j <= i; j++) {
+                            var before = data[j];
+                            var before_date = getDateFromEntry(before);
+                            s_squares += Math.pow(before.glucose / scaleBg - y_mean, 2);
+                            var delta_t = (before_date - time_0) / 1000 / scaleTime;
+                            var bg_j = a * Math.pow(delta_t, 2) + b * delta_t + c;
+                            s_residual_squares += Math.pow(before.glucose / scaleBg - bg_j, 2);
+                        }
+                        var r_squ = 0.64;
+                        if (s_squares != 0) {
+                            r_squ = 1 - s_residual_squares / s_squares;
+                        }
+                        if (n > 3) {
+                            if (r_squ >= corrMax) {
+                                corrMax = r_squ;
+                                // double delta_t = (then_date - time_0) / 1000;
+                                dura_p = -ti * scaleTime / 60;            // remember we are going backwards in time
+                                var delta5Min = 5 * 60 / scaleTime;
+                                delta_pl =-scaleBg * (a * Math.pow(- delta5Min, 2) - b * delta5Min);     // 5 minute slope from last fitted bg starting from last bg, i.e. t=0
+                                delta_pn = scaleBg * (a * Math.pow( delta5Min, 2) + b * delta5Min);     // 5 minute slope to next fitted bg starting from last bg, i.e. t=0
+                                bg_acceleration = 2 * a * scaleBg;             // 2nd derivative of parabola per (5min)^2
+                                a_0 = c * scaleBg;
+                                a_1 = b * scaleBg;
+                                a_2 = a * scaleBg;
+                                //r_squ = corrMax;
+                                best_a = a * scaleBg;
+                                best_b = b * scaleBg;
+                                best_c = c * scaleBg;
+                            }
+                        }
+                    }
+                }
+                pp_debug += "coeffs a/b/c=(" + round(best_a,2) + " / " + round(best_b,2) + " / " + round(best_c,2) + "); bg date=" + time_0 + "; ";
+                pp_debug += "Parabola Fits a0/a1/a2=(" + round(a_0,2) + " / " + round(a_1,2) + " / " + round(a_2,2) + "); ";
+            }
+           pp_debug += "Slopes 05/15/40=(" + round(slope05,2) + " / " + round(slope15,2) + " / " + round(slope40,2) + "); "
+    return {
+        delta: Math.round( last_delta * 10000 ) / 10000
+        , glucose: Math.round( now.glucose * 10000 ) / 10000
+        , noise: Math.round(now.noise)
+        , short_avgdelta: Math.round( short_avgdelta * 10000 ) / 10000
+        , long_avgdelta: Math.round( long_avgdelta * 10000 ) / 10000
+        // autoISF values to return to determineBasal.js
+        , autoISF_average: Math.round( autoISF_average * 10000) / 10000
+        , autoISF_duration: Math.round(autoISF_duration * 10000) / 10000
+        , dura_p: Math.round( dura_p * 10000) / 10000
+        , delta_pl: Math.round( delta_pl * 10000) / 10000
+        , delta_pn: Math.round( delta_pn * 10000) / 10000
+        , bg_acceleration: bg_acceleration
+        , r_squ: Math.round( corrMax * 10000) / 10000
+        , parabola_fit_a0: Math.round( a_0 * 10000) / 10000
+        , parabola_fit_a1: Math.round( a_1 * 10000) / 10000
+        , parabola_fit_a2: Math.round( a_2 * 10000) / 10000
+        , pp_debug
+        // end autoISF values
+        , date: now_date
+        , last_cal: last_cal
+        , device: now.device
+    };
+};
+
+module.exports = getLastGlucose;

+ 246 - 0
oref0/lib/lib/glucose-stats.js

@@ -0,0 +1,246 @@
+
+
+const moment = require('moment');
+
+const log = console.error;
+
+/* eslint-disable-next-line no-unused-vars */
+const error = console.error;
+const debug = console.error;
+
+module.exports = {};
+const calcStatsExports = module.exports;
+
+// Calculate the sum of the distance of all points (sod)
+// Calculate the overall distance between the first and the last point (overallDistance)
+// Calculate the noise as the following formula: 1 - sod / overallDistance
+// Noise will get closer to zero as the sum of the individual lines are mostly
+// in a straight or straight moving curve
+// Noise will get closer to one as the sum of the distance of the individual lines get large
+// Also add multiplier to get more weight to the latest BG values
+// Also added weight for points where the delta shifts from pos to neg or neg to pos (peaks/valleys)
+// the more peaks and valleys, the more noise is amplified
+// Input:
+// [
+//   {
+//     real glucose   -- glucose value in mg/dL
+//     real readDate  -- milliseconds since Epoch
+//   },...
+// ]
+const calcNoise = (sgvArr) => {
+  let noise = 0;
+
+  const n = sgvArr.length;
+
+  const firstSGV = sgvArr[0].glucose * 1000.0;
+  const firstTime = sgvArr[0].readDate / 1000.0 * 30.0;
+
+  const lastSGV = sgvArr[n - 1].glucose * 1000.0;
+  const lastTime = sgvArr[n - 1].readDate / 1000.0 * 30.0;
+
+  const xarr = [];
+
+  for (let i = 0; i < n; i += 1) {
+    xarr.push(sgvArr[i].readDate / 1000.0 * 30.0 - firstTime);
+  }
+
+  // sod = sum of distances
+  let sod = 0;
+  let lastDelta = 0;
+
+  for (let i = 1; i < n; i += 1) {
+    // y2y1Delta adds a multiplier that gives
+    // higher priority to the latest BG's
+    let y2y1Delta = (sgvArr[i].glucose - sgvArr[i - 1].glucose) * 1000.0 * (1 + i / (n * 3));
+
+    const x2x1Delta = xarr[i] - xarr[i - 1];
+
+    if ((lastDelta > 0) && (y2y1Delta < 0)) {
+      // switched from positive delta to negative, increase noise impact
+      y2y1Delta *= 1.1;
+    } else if ((lastDelta < 0) && (y2y1Delta > 0)) {
+      // switched from negative delta to positive, increase noise impact
+      y2y1Delta *= 1.2;
+    }
+
+    lastDelta = y2y1Delta;
+
+    sod += Math.sqrt(Math.pow(x2x1Delta, 2) + Math.pow(y2y1Delta, 2));
+  }
+
+  const overallsod = Math.sqrt(Math.pow(lastSGV - firstSGV, 2) + Math.pow(lastTime - firstTime, 2));
+
+  if (sod === 0) {
+    // protect from divide by 0
+    noise = 0;
+  } else {
+    noise = 1 - (overallsod / sod);
+  }
+
+  return noise;
+};
+
+calcStatsExports.calcSensorNoise = (calcGlucose, glucoseHist, lastCal, sgv) => {
+  const MAXRECORDS = 8;
+  const MINRECORDS = 4;
+  const sgvArr = [];
+
+  const numRecords = Math.max(glucoseHist.length - MAXRECORDS, 0);
+
+  for (let i = numRecords; i < glucoseHist.length; i += 1) {
+    // Only use values that are > 30 to filter out invalid values.
+    if (lastCal && (glucoseHist[i].glucose > 30) && ('unfiltered' in glucoseHist[i]) && (glucoseHist[i].unfiltered > 100)) {
+      // use the unfiltered data with the most recent calculated calibration value
+      // this will provide a noise calculation that is independent of calibration jumps
+      sgvArr.push({
+        glucose: calcGlucose(glucoseHist[i], lastCal),
+        readDate: glucoseHist[i].readDateMills,
+      });
+    } else if (glucoseHist[i].glucose > 30) {
+      // if raw data isn't available, use the transmitter calibrated glucose
+      sgvArr.push({
+        glucose: glucoseHist[i].glucose,
+        readDate: glucoseHist[i].readDateMills,
+      });
+    }
+  }
+
+  if (sgv) {
+    if (lastCal && 'unfiltered' in sgv && sgv.unfiltered > 100) {
+      sgvArr.push({
+        glucose: calcGlucose(sgv, lastCal),
+        readDate: sgv.readDateMills,
+      });
+    } else {
+      sgvArr.push({
+        glucose: sgv.glucose,
+        readDate: sgv.readDateMills,
+      });
+    }
+  }
+  if (sgvArr.length < MINRECORDS) {
+    return 0;
+  }
+  return calcNoise(sgvArr);
+};
+
+// Return 10 minute trend total
+calcStatsExports.calcTrend = (calcGlucose, glucoseHist, lastCal, sgv) => {
+  let sgvHist = null;
+
+  let trend = 0;
+
+  if (glucoseHist.length > 0) {
+    let maxDate = null;
+    let timeSpan = 0;
+    let totalDelta = 0;
+    const currentTime = sgv ? moment(sgv.readDateMills)
+      : moment(glucoseHist[glucoseHist.length - 1].readDateMills);
+
+    sgvHist = [];
+
+    // delete any deltas > 16 minutes and any that don't have an unfiltered value (backfill records)
+    let minDate = currentTime.valueOf() - 16 * 60 * 1000;
+    for (let i = 0; i < glucoseHist.length; i += 1) {
+      if (lastCal && (glucoseHist[i].readDateMills >= minDate) && ('unfiltered' in glucoseHist[i]) && (glucoseHist[i].unfiltered > 100)) {
+        sgvHist.push({
+          glucose: calcGlucose(glucoseHist[i], lastCal),
+          readDate: glucoseHist[i].readDateMills,
+        });
+      } else if (glucoseHist[i].readDateMills >= minDate) {
+        sgvHist.push({
+          glucose: glucoseHist[i].glucose,
+          readDate: glucoseHist[i].readDateMills,
+        });
+      }
+    }
+
+    if (sgv) {
+      if (lastCal && ('unfiltered' in sgv) && (sgv.unfiltered > 100)) {
+        sgvHist.push({
+          glucose: calcGlucose(sgv, lastCal),
+          readDate: sgv.readDateMills,
+        });
+      } else {
+        sgvHist.push({
+          glucose: sgv.glucose,
+          readDate: sgv.readDateMills,
+        });
+      }
+    }
+
+    if (sgvHist.length > 1) {
+      minDate = sgvHist[0].readDate;
+      maxDate = sgvHist[sgvHist.length - 1].readDate;
+
+      // Use the current calibration value to calculate the glucose from the
+      // unfiltered data. This allows the trend calculation to be independent
+      // of the calibration jumps
+      totalDelta = sgvHist[sgvHist.length - 1].glucose - sgvHist[0].glucose;
+
+      timeSpan = (maxDate - minDate) / 1000.0 / 60.0;
+
+      trend = 10 * totalDelta / timeSpan;
+    }
+  } else {
+    debug(`Not enough history for trend calculation: ${glucoseHist.length}`);
+  }
+
+  return trend;
+};
+
+// Return sensor noise
+calcStatsExports.calcNSNoise = (noise, glucoseHist) => {
+  let nsNoise = 0; // Unknown
+  const currSGV = glucoseHist[glucoseHist.length - 1];
+  let deltaSGV = 0;
+
+  if (glucoseHist.length > 1) {
+    const priorSGV = glucoseHist[glucoseHist.length - 2];
+
+    if ((currSGV.glucose > 30) && (priorSGV.glucose > 30)) {
+      deltaSGV = currSGV.glucose - priorSGV.glucose;
+    }
+  }
+
+  if (!currSGV) {
+    nsNoise = 1;
+  } else if (currSGV.glucose > 400) {
+    log(`Glucose ${currSGV.glucose} > 400 - setting noise level Heavy`);
+    nsNoise = 4;
+  } else if (currSGV.glucose < 40) {
+    log(`Glucose ${currSGV.glucose} < 40 - setting noise level Light`);
+    nsNoise = 2;
+  } else if (Math.abs(deltaSGV) > 30) {
+    // This is OK even during a calibration jump because we don't want OpenAPS to be too
+    // agressive with the "false" trend implied by a large positive jump
+    log(`Glucose change ${deltaSGV} out of range [-30, 30] - setting noise level Heavy`);
+    nsNoise = 4;
+  } else if (noise < 0.35) {
+    nsNoise = 1; // Clean
+  } else if (noise < 0.5) {
+    nsNoise = 2; // Light
+  } else if (noise < 0.7) {
+    nsNoise = 3; // Medium
+  } else if (noise >= 0.7) {
+    nsNoise = 4; // Heavy
+  }
+
+  return nsNoise;
+};
+
+calcStatsExports.NSNoiseString = (nsNoise) => {
+  switch (nsNoise) {
+    case 1:
+      return 'Clean';
+    case 2:
+      return 'Light';
+    case 3:
+      return 'Medium';
+    case 4:
+      return 'Heavy';
+    case 0:
+    default:
+      return 'Unknown';
+  }
+};

+ 144 - 0
oref0/lib/lib/iob/calculate.js

@@ -0,0 +1,144 @@
+function iobCalc(treatment, time, curve, dia, peak, profile) {
+    // iobCalc returns two variables:
+    //   activityContrib = units of treatment.insulin used in previous minute
+    //   iobContrib = units of treatment.insulin still remaining at a given point in time
+    // ("Contrib" is used because these are the amounts contributed from pontentially multiple treatment.insulin dosages -- totals are calculated in total.js)
+    //
+    // Variables can be calculated using either:
+    //   A bilinear insulin action curve (which only takes duration of insulin activity (dia) as an input parameter) or
+    //   An exponential insulin action curve (which takes both a dia and a peak parameter)
+    // (which functional form to use is specified in the user's profile)
+
+    if (treatment.insulin) {
+
+        // Calc minutes since bolus (minsAgo)
+        if (typeof time === 'undefined') {
+            time = new Date();
+        }
+        var bolusTime = new Date(treatment.date);
+        var minsAgo = Math.round((time - bolusTime) / 1000 / 60);
+
+
+        if (curve === 'bilinear') {
+            return iobCalcBilinear(treatment, minsAgo, dia);  // no user-specified peak with this model
+        } else {
+            return iobCalcExponential(treatment, minsAgo, dia, peak, profile);
+        }
+
+    } else { // empty return if (treatment.insulin) == False
+        return {};
+    }    
+}
+
+
+function iobCalcBilinear(treatment, minsAgo, dia) {
+    
+    var default_dia = 3.0 // assumed duration of insulin activity, in hours
+    var peak = 75;        // assumed peak insulin activity, in minutes
+    var end = 180;        // assumed end of insulin activity, in minutes
+
+    // Scale minsAgo by the ratio of the default dia / the user's dia 
+    // so the calculations for activityContrib and iobContrib work for 
+    // other dia values (while using the constants specified above)
+    var timeScalar = default_dia / dia; 
+    var scaled_minsAgo = timeScalar * minsAgo;
+
+
+    var activityContrib = 0;  
+    var iobContrib = 0;       
+
+    // Calc percent of insulin activity at peak, and slopes up to and down from peak
+    // Based on area of triangle, because area under the insulin action "curve" must sum to 1
+    // (length * height) / 2 = area of triangle (1), therefore height (activityPeak) = 2 / length (which in this case is dia, in minutes)
+    // activityPeak scales based on user's dia even though peak and end remain fixed
+    var activityPeak = 2 / (dia * 60)  
+    var slopeUp = activityPeak / peak
+    var slopeDown = -1 * (activityPeak / (end - peak))
+
+    if (scaled_minsAgo < peak) {
+
+        activityContrib = treatment.insulin * (slopeUp * scaled_minsAgo);
+
+        var x1 = (scaled_minsAgo / 5) + 1;  // scaled minutes since bolus, pre-peak; divided by 5 to work with coefficients estimated based on 5 minute increments
+        iobContrib = treatment.insulin * ( (-0.001852*x1*x1) + (0.001852*x1) + 1.000000 );
+
+    } else if (scaled_minsAgo < end) {
+        
+        var minsPastPeak = scaled_minsAgo - peak
+        activityContrib = treatment.insulin * (activityPeak + (slopeDown * minsPastPeak));
+
+        var x2 = ((scaled_minsAgo - peak) / 5);  // scaled minutes past peak; divided by 5 to work with coefficients estimated based on 5 minute increments
+        iobContrib = treatment.insulin * ( (0.001323*x2*x2) + (-0.054233*x2) + 0.555560 );
+    }
+
+    return {
+        activityContrib: activityContrib,
+        iobContrib: iobContrib        
+    };
+}
+
+
+function iobCalcExponential(treatment, minsAgo, dia, peak, profile) {
+
+    // Use custom peak time (in minutes) if value is valid
+    if ( profile.curve === "rapid-acting" ) {
+        if (profile.useCustomPeakTime === true && profile.insulinPeakTime !== undefined) {
+            if ( profile.insulinPeakTime > 120 ) {
+                console.error('Setting maximum Insulin Peak Time of 120m for',profile.curve,'insulin');
+                peak = 120;
+            } else if ( profile.insulinPeakTime < 50 ) {
+                console.error('Setting minimum Insulin Peak Time of 50m for',profile.curve,'insulin');
+                peak = 50;
+            } else {
+                peak = profile.insulinPeakTime;
+            }
+        } else {
+            peak = 75;
+        }
+    } else if ( profile.curve === "ultra-rapid" ) {
+        if (profile.useCustomPeakTime === true && profile.insulinPeakTime !== undefined) {
+            if ( profile.insulinPeakTime > 100 ) {
+                console.error('Setting maximum Insulin Peak Time of 100m for',profile.curve,'insulin');
+                peak = 100;
+            } else if ( profile.insulinPeakTime < 35 ) {
+                console.error('Setting minimum Insulin Peak Time of 35m for',profile.curve,'insulin');
+                peak = 35;
+            } else {
+                peak = profile.insulinPeakTime;
+            }
+        } else {
+            peak = 55;
+        }
+    } else {
+        console.error('Curve of',profile.curve,'is not supported.');
+    }
+    var end = dia * 60;  // end of insulin activity, in minutes
+
+
+    var activityContrib = 0;  
+    var iobContrib = 0;       
+
+    if (minsAgo < end) {
+        
+        // Formula source: https://github.com/LoopKit/Loop/issues/388#issuecomment-317938473
+        // Mapping of original source variable names to those used here:
+        //   td = end
+        //   tp = peak
+        //   t  = minsAgo
+        var tau = peak * (1 - peak / end) / (1 - 2 * peak / end);  // time constant of exponential decay
+        var a = 2 * tau / end;                                     // rise time factor
+        var S = 1 / (1 - a + (1 + a) * Math.exp(-end / tau));      // auxiliary scale factor
+        
+        activityContrib = treatment.insulin * (S / Math.pow(tau, 2)) * minsAgo * (1 - minsAgo / end) * Math.exp(-minsAgo / tau);
+        iobContrib = treatment.insulin * (1 - S * (1 - a) * ((Math.pow(minsAgo, 2) / (tau * end * (1 - a)) - minsAgo / tau - 1) * Math.exp(-minsAgo / tau) + 1));
+        //console.error('DIA: ' + dia + ' minsAgo: ' + minsAgo + ' end: ' + end + ' peak: ' + peak + ' tau: ' + tau + ' a: ' + a + ' S: ' + S + ' activityContrib: ' + activityContrib + ' iobContrib: ' + iobContrib);
+    }
+
+    return {
+        activityContrib: activityContrib,
+        iobContrib: iobContrib        
+    };
+}
+
+
+exports = module.exports = iobCalc;

+ 570 - 0
oref0/lib/lib/iob/history.js

@@ -0,0 +1,570 @@
+
+var tz = require('moment-timezone');
+var basalprofile = require('../profile/basal.js');
+var _ = require('lodash');
+var moment = require('moment');
+
+function splitTimespanWithOneSplitter(event,splitter) {
+
+    var resultArray = [event];
+
+    if (splitter.type === 'recurring') {
+
+        var startMinutes = event.started_at.getHours() * 60 + event.started_at.getMinutes();
+        var endMinutes = startMinutes + event.duration;
+
+        // 1440 = one day; no clean way to check if the event overlaps midnight
+        // so checking if end of event in minutes is past midnight
+
+        if (event.duration > 30 || (startMinutes < splitter.minutes && endMinutes > splitter.minutes) || (endMinutes > 1440 && splitter.minutes < (endMinutes - 1440))) {
+
+            var event1 = _.cloneDeep(event);
+            var event2 = _.cloneDeep(event);
+
+            var event1Duration = 0;
+
+            if (event.duration > 30) {
+                event1Duration = 30;
+            } else {
+                var splitPoint = splitter.minutes;
+                if (endMinutes > 1440) { splitPoint = 1440; }
+                event1Duration = splitPoint - startMinutes;
+            }
+
+            var event1EndDate = moment(event.started_at).add(event1Duration,'minutes');
+
+            event1.duration = event1Duration;
+
+            event2.duration  = event.duration - event1Duration;
+            event2.timestamp = event1EndDate.format();
+            event2.started_at = new Date(event2.timestamp);
+            event2.date = event2.started_at.getTime();
+
+            resultArray = [event1,event2];
+        }
+    }
+
+    return resultArray;
+}
+
+function splitTimespan(event, splitterMoments) {
+
+    var results = [event];
+
+    var splitFound = true;
+
+    while(splitFound) {
+
+        var resultArray = [];
+        splitFound = false;
+
+        _.forEach(results,function split(o) {
+            _.forEach(splitterMoments,function split(p) {
+                var splitResult = splitTimespanWithOneSplitter(o,p);
+                if (splitResult.length > 1) {
+                    resultArray = resultArray.concat(splitResult);
+                    splitFound = true;
+                    return false;
+                }
+            });
+
+            if (!splitFound) resultArray = resultArray.concat([o]);
+
+        });
+
+        results = resultArray;
+    }
+
+    return results;
+}
+
+// Split currentEvent around any conflicting suspends
+// by removing the time period from the event that
+// overlaps with any suspend.
+function splitAroundSuspends (currentEvent, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended) {
+    var events = [];
+
+    var firstResumeStarted = new Date(firstResumeTime);
+    var firstResumeDate = firstResumeStarted.getTime()
+
+    var lastSuspendStarted = new Date(lastSuspendTime);
+    var lastSuspendDate = lastSuspendStarted.getTime();
+
+    if (suspendedPrior && (currentEvent.date < firstResumeDate)) {
+        if ((currentEvent.date+currentEvent.duration*60*1000) < firstResumeDate) {
+            currentEvent.duration = 0;
+        } else {
+            currentEvent.duration = ((currentEvent.date+currentEvent.duration*60*1000)-firstResumeDate)/60/1000;
+
+            currentEvent.started_at = new Date(tz(firstResumeTime));
+            currentEvent.date = firstResumeDate
+        }
+    }
+
+    if (currentlySuspended && ((currentEvent.date+currentEvent.duration*60*1000) > lastSuspendTime)) {
+        if (currentEvent.date > lastSuspendTime) {
+            currentEvent.duration = 0;
+        } else {
+            currentEvent.duration = (firstResumeDate - currentEvent.date)/60/1000;
+        }
+    }
+
+    events.push(currentEvent);
+
+    if (currentEvent.duration === 0) {
+        // bail out rather than wasting time going through the rest of the suspend events
+        return events;
+    }
+
+    for (var i=0; i < pumpSuspends.length; i++) {
+        var suspend = pumpSuspends[i];
+
+        for (var j=0; j < events.length; j++) {
+
+            if ((events[j].date <= suspend.date) && (events[j].date+events[j].duration*60*1000) > suspend.date) {
+                // event started before the suspend, but finished after the suspend started
+
+                if ((events[j].date+events[j].duration*60*1000) > (suspend.date+suspend.duration*60*1000)) {
+                    var event2 = _.cloneDeep(events[j]);
+
+                    var event2StartDate = moment(suspend.started_at).add(suspend.duration,'minutes');
+
+                    event2.timestamp = event2StartDate.format();
+                    event2.started_at = new Date(tz(event2.timestamp));
+                    event2.date = suspend.date+suspend.duration*60*1000;
+
+                    event2.duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000;
+
+                    events.push(event2);
+                }
+
+                events[j].duration = (suspend.date-events[j].date)/60/1000;
+
+            } else if ((suspend.date <= events[j].date) && (suspend.date+suspend.duration*60*1000 > events[j].date)) {
+                // suspend started before the event, but finished after the event started
+            
+                events[j].duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000;
+
+                var eventStartDate = moment(suspend.started_at).add(suspend.duration,'minutes');
+
+                events[j].timestamp = eventStartDate.format();
+                events[j].started_at = new Date(tz(events[j].timestamp));
+                events[j].date = suspend.date + suspend.duration*60*1000;
+            }
+        }
+    }
+
+    return events;
+}
+
+function calcTempTreatments (inputs, zeroTempDuration) {
+    var pumpHistory = inputs.history;
+    var pumpHistory24 = inputs.history24;
+    var profile_data = inputs.profile;
+    var autosens_data = inputs.autosens;
+    var tempHistory = [];
+    var tempBoluses = [];
+    var pumpSuspends = [];
+    var pumpResumes = [];
+    var suspendedPrior = false;
+    var firstResumeTime, lastSuspendTime;
+    var currentlySuspended = false;
+    var suspendError = false;
+
+    var now = new Date(tz(inputs.clock));
+
+    if(inputs.history24) {
+        var pumpHistory =  [ ].concat(inputs.history).concat(inputs.history24);
+    }
+
+    var lastRecordTime = now;
+
+    // Gather the times the pump was suspended and resumed
+    for (var i=0; i < pumpHistory.length; i++) {
+        var temp = {};
+
+        var current = pumpHistory[i];
+
+        if (current._type === "PumpSuspend") {
+            temp.timestamp = current.timestamp;
+            temp.started_at = new Date(tz(current.timestamp));
+            temp.date = temp.started_at.getTime();
+            pumpSuspends.push(temp);
+        } else if (current._type === "PumpResume") {
+            temp.timestamp = current.timestamp;
+            temp.started_at = new Date(tz(current.timestamp));
+            temp.date = temp.started_at.getTime();
+            pumpResumes.push(temp);
+        }
+    }
+
+    pumpSuspends = _.sortBy(pumpSuspends, 'date');
+
+    pumpResumes = _.sortBy(pumpResumes, 'date');
+
+    if (pumpResumes.length > 0) {
+        firstResumeTime = pumpResumes[0].timestamp;
+
+        // Check to see if our first resume was prior to our first suspend
+        // indicating suspend was prior to our first event.
+        if (pumpSuspends.length === 0 || (pumpResumes[0].date < pumpSuspends[0].date)) {
+            suspendedPrior = true;
+        }
+
+    }
+
+    var j=0;  // matching pumpResumes entry;
+
+    // Match the resumes with the suspends to get durations
+    for (i=0; i < pumpSuspends.length; i++) {
+        for (; j < pumpResumes.length; j++) {
+            if (pumpResumes[j].date > pumpSuspends[i].date) {
+                break;
+            }
+        }
+
+        if ((j >= pumpResumes.length) && !currentlySuspended) {
+            // even though it isn't the last suspend, we have reached
+            // the final suspend. Set resume last so the
+            // algorithm knows to suspend all the way
+            // through the last record beginning at the last suspend
+            // since we don't have a matching resume.
+            currentlySuspended = 1;
+            lastSuspendTime = pumpSuspends[i].timestamp;
+
+            break;
+        }
+
+        pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date)/60/1000;
+
+    }
+
+    // These checks indicate something isn't quite aligned.
+    // Perhaps more resumes that suspends or vice versa...
+    if (!suspendedPrior && !currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) {
+        console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+")!");
+    } else if (suspendedPrior && !currentlySuspended && ((pumpResumes.length-1) !== pumpSuspends.length)) {
+        console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to history block!");
+    } else if (!suspendedPrior && currentlySuspended && (pumpResumes.length !== (pumpSuspends.length-1))) {
+        console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended past end of history block!");
+    } else if (suspendedPrior && currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) {
+        console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to and past end of history block!");
+    }
+
+    if (i < (pumpSuspends.length-1)) {
+        // truncate any extra suspends. if we had any extras
+        // the error checks above would have issued a error log message
+        pumpSuspends.splice(i+1, pumpSuspends.length-i-1);
+    }
+
+    // Pick relevant events for processing and clean the data
+
+    for (i=0; i < pumpHistory.length; i++) {
+        var current = pumpHistory[i];
+        if (current.bolus && current.bolus._type === "Bolus") {
+            var temp = current;
+            current = temp.bolus;
+        }
+        if (current.created_at) {
+            current.timestamp = current.created_at;
+        }
+        var currentRecordTime = new Date(tz(current.timestamp));
+        //console.error(current);
+        //console.error(currentRecordTime,lastRecordTime);
+        // ignore duplicate or out-of-order records (due to 1h and 24h overlap, or timezone changes)
+        if (currentRecordTime > lastRecordTime) {
+            //console.error("",currentRecordTime," > ",lastRecordTime);
+            //process.stderr.write(".");
+            continue;
+        } else {
+            lastRecordTime = currentRecordTime;
+        }
+        if (current._type === "Bolus") {
+            var temp = {};
+            temp.timestamp = current.timestamp;
+            temp.started_at = new Date(tz(current.timestamp));
+            if (temp.started_at > now) {
+                //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at);
+                process.stderr.write(" "+current.amount+"U @ "+temp.started_at);
+            } else {
+                temp.date = temp.started_at.getTime();
+                temp.insulin = current.amount;
+                tempBoluses.push(temp);
+            }
+        } else if (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard") {
+            //imports treatments entered through Nightscout Care Portal
+            //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard
+            var temp = {};
+            temp.timestamp = current.created_at;
+            temp.started_at = new Date(tz(temp.timestamp));
+            temp.date = temp.started_at.getTime();
+            temp.insulin = current.insulin;
+            tempBoluses.push(temp);
+        } else if (current.enteredBy === "xdrip") {
+            var temp = {};
+            temp.timestamp = current.timestamp;
+            temp.started_at = new Date(tz(temp.timestamp));
+            temp.date = temp.started_at.getTime();
+            temp.insulin = current.insulin;
+            tempBoluses.push(temp);
+        } else if (current.enteredBy ==="HAPP_App" && current.insulin) {
+            var temp = {};
+            temp.timestamp = current.created_at;
+            temp.started_at = new Date(tz(temp.timestamp));
+            temp.date = temp.started_at.getTime();
+            temp.insulin = current.insulin;
+            tempBoluses.push(temp);
+        } else if (current.eventType === "Temp Basal" && (current.enteredBy === "HAPP_App" || current.enteredBy === "openaps://AndroidAPS")) {
+            var temp = {};
+            temp.rate = current.absolute;
+            temp.duration = current.duration;
+            temp.timestamp = current.created_at;
+            temp.started_at = new Date(tz(temp.timestamp));
+            temp.date = temp.started_at.getTime();
+            tempHistory.push(temp);
+        } else if (current.eventType === "Temp Basal") {
+            var temp = {};
+            temp.rate = current.rate;
+            temp.duration = current.duration;
+            // Loop reports the amount of insulin actually delivered while the temp basal was running
+            // use that to calculate the effective temp basal rate
+            if (typeof current.amount !== 'undefined') {
+                temp.rate = current.amount / current.duration * 60;
+            }
+            temp.timestamp = current.timestamp;
+            temp.started_at = new Date(tz(temp.timestamp));
+            temp.date = temp.started_at.getTime();
+            tempHistory.push(temp);
+        } else if (current._type === "TempBasal") {
+            if (current.temp === 'percent') {
+                continue;
+            }
+            var rate = current.rate;
+            var timestamp = current.timestamp;
+            var duration;
+            if (i>0 && pumpHistory[i-1].timestamp === timestamp && pumpHistory[i-1]._type === "TempBasalDuration") {
+                duration = pumpHistory[i-1]['duration (min)'];
+            } else {
+                for (var iter=0; iter < pumpHistory.length; iter++) {
+                    if (pumpHistory[iter].timestamp === timestamp && pumpHistory[iter]._type === "TempBasalDuration") {
+                            duration = pumpHistory[iter]['duration (min)'];
+                            break;
+                    }
+                }
+
+                if (duration === undefined) {
+                    console.error("No duration found for "+rate+" U/hr basal "+timestamp, pumpHistory[i - 1], current, pumpHistory[i + 1]);
+                }
+            }
+            var temp = {};
+            temp.rate = rate;
+            temp.timestamp = current.timestamp;
+            temp.started_at = new Date(tz(temp.timestamp));
+            temp.date = temp.started_at.getTime();
+            temp.duration = duration;
+            tempHistory.push(temp);
+        }
+        // Add a temp basal cancel event to ignore future temps and reduce predBG oscillation
+        var temp = {};
+        temp.rate = 0;
+        // start the zero temp 1m in the future to avoid clock skew
+        temp.started_at = new Date(now.getTime() + (1 * 60 * 1000));
+        temp.date = temp.started_at.getTime();
+        if (zeroTempDuration) {
+            temp.duration = zeroTempDuration;
+        } else {
+            temp.duration = 0;
+        }
+        tempHistory.push(temp);
+    }
+
+    // Check for overlapping events and adjust event lengths in case of overlap
+
+    tempHistory = _.sortBy(tempHistory, 'date');
+
+    for (i=0; i+1 < tempHistory.length; i++) {
+        if (tempHistory[i].date + tempHistory[i].duration*60*1000 > tempHistory[i+1].date) {
+            tempHistory[i].duration = (tempHistory[i+1].date - tempHistory[i].date)/60/1000;
+            // Delete AndroidAPS "Cancel TBR records" in which duration is not populated
+            if (tempHistory[i+1].duration === null) {
+                tempHistory.splice(i+1, 1);
+            }
+        }
+    }
+
+    // Create an array of moments to slit the temps by
+    // currently supports basal changes
+
+    var splitterEvents = [];
+
+    _.forEach(profile_data.basalprofile,function addSplitter(o) {
+        var splitterEvent = {};
+        splitterEvent.type = 'recurring';
+        splitterEvent.minutes = o.minutes;
+        splitterEvents.push(splitterEvent);
+    });
+
+    // iterate through the events and split at basal break points if needed
+
+    var splitHistoryByBasal = [];
+
+    _.forEach(tempHistory, function splitEvent(o) {
+        splitHistoryByBasal = splitHistoryByBasal.concat(splitTimespan(o,splitterEvents));
+    });
+
+    tempHistory = _.sortBy(tempHistory, function(o) { return o.date; });
+
+    var suspend_zeros_iob = false;
+
+    if (typeof profile_data.suspend_zeros_iob !== 'undefined') {
+        suspend_zeros_iob = profile_data.suspend_zeros_iob;
+    }
+
+    if (suspend_zeros_iob) {
+        // iterate through the events and adjust their 
+        // times as required to account for pump suspends
+        var splitHistory = [];
+
+        _.forEach(splitHistoryByBasal, function splitSuspendEvent(o) {
+            var splitEvents = splitAroundSuspends(o, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended);
+            splitHistory = splitHistory.concat(splitEvents);
+        });
+
+        var zTempSuspendBasals = [];
+
+        // Any existing temp basals during times the pump was suspended are now deleted
+        // Add 0 temp basals to negate the profile basal rates during times pump is suspended
+        _.forEach(pumpSuspends, function createTempBasal(o) {
+            var zTempBasal = [{
+                _type: 'SuspendBasal',
+                rate: 0,
+                duration: o.duration,
+                date: o.date,
+                started_at: o.started_at
+            }];
+            zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal);
+        });
+
+        // Add temp suspend basal for maximum DIA (8) up to the resume time
+        // if there is no matching suspend in the history before the first
+        // resume
+        var max_dia_ago = now.getTime() - 8*60*60*1000;
+        var firstResumeStarted = new Date(firstResumeTime);
+        var firstResumeDate = firstResumeStarted.getTime()
+
+        // impact on IOB only matters if the resume occurred
+        // after DIA hours before now.
+        // otherwise, first resume date can be ignored. Whatever
+        // insulin is present prior to resume will be aged
+        // out due to DIA.
+        if (suspendedPrior && (max_dia_ago < firstResumeDate)) {
+
+            var suspendStart = new Date(max_dia_ago);
+            var suspendStartDate = suspendStart.getTime()
+            var started_at = new Date(tz(suspendStart.toISOString()));
+
+            var zTempBasal = [{
+               // add _type to aid debugging. It isn't used
+               // anywhere.
+                _type: 'SuspendBasal',
+                rate: 0,
+                duration: (firstResumeDate - max_dia_ago)/60/1000,
+                date: suspendStartDate,
+                started_at: started_at
+            }];
+            zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal);
+        }
+
+        if (currentlySuspended) {
+            var suspendStart = new Date(lastSuspendTime);
+            var suspendStartDate = suspendStart.getTime()
+            var started_at = new Date(tz(suspendStart.toISOString()));
+
+            var zTempBasal = [{
+                _type: 'SuspendBasal',
+                rate: 0,
+                duration: (now - suspendStartDate)/60/1000,
+                date: suspendStartDate,
+                timestamp: lastSuspendTime,
+                started_at: started_at
+            }];
+            zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal);
+        }
+
+        // Add the new 0 temp basals to the splitHistory.
+        // We have to split the new zero temp basals by the profile
+        // basals just like the other temp basals.
+        _.forEach(zTempSuspendBasals, function splitEvent(o) {
+            splitHistory = splitHistory.concat(splitTimespan(o,splitterEvents));
+        });
+    } else {
+        splitHistory = splitHistoryByBasal;
+    }
+
+    splitHistory = _.sortBy(splitHistory, function(o) { return o.date; });
+
+    // tempHistory = splitHistory;
+
+    // iterate through the temp basals and create bolus events from temps that affect IOB
+
+    var tempBolusSize;
+
+    for (i=0; i < splitHistory.length; i++) {
+
+        var currentItem = splitHistory[i];
+
+        if (currentItem.duration > 0) {
+
+            var currentRate = profile_data.current_basal;
+            if (!_.isEmpty(profile_data.basalprofile)) {
+                currentRate = basalprofile.basalLookup(profile_data.basalprofile,new Date(currentItem.timestamp));
+            }
+
+            if (typeof profile_data.min_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') {
+                target_bg = (profile_data.min_bg + profile_data.max_bg) / 2;
+            }
+            //if (profile_data.temptargetSet && target_bg > 110) {
+                //sensitivityRatio = 2/(2+(target_bg-100)/40);
+                //currentRate = profile_data.current_basal * sensitivityRatio;
+            //}
+            var sensitivityRatio;
+            var profile = profile_data;
+            var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled basal (which might change)
+            if ( profile.half_basal_exercise_target ) {
+                var halfBasalTarget = profile.half_basal_exercise_target;
+            } else {
+                var halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%)
+            }
+            if ( profile.exercise_mode && profile.temptargetSet && target_bg >= normalTarget + 5 ) {
+                // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44
+                // e.g.: Sensitivity ratio set to 0.8 based on temp target of 120; Adjusting basal from 1.65 to 1.35; ISF from 58.9 to 73.6
+                var c = halfBasalTarget - normalTarget;
+                sensitivityRatio = c/(c+target_bg-normalTarget);
+            } else if (typeof autosens_data !== 'undefined' ) {
+                sensitivityRatio = autosens_data.ratio;
+                //process.stderr.write("Autosens ratio: "+sensitivityRatio+"; ");
+            }
+            if ( sensitivityRatio ) {
+                currentRate = currentRate * sensitivityRatio;
+            }
+
+            var netBasalRate = currentItem.rate - currentRate;
+            if (netBasalRate < 0) { tempBolusSize = -0.05; }
+            else { tempBolusSize = 0.05; }
+            var netBasalAmount = Math.round(netBasalRate*currentItem.duration*10/6)/100
+            var tempBolusCount = Math.round(netBasalAmount / tempBolusSize);
+            var tempBolusSpacing = currentItem.duration / tempBolusCount;
+            for (j=0; j < tempBolusCount; j++) {
+                var tempBolus = {};
+                tempBolus.insulin = tempBolusSize;
+                tempBolus.date = currentItem.date + j * tempBolusSpacing*60*1000;
+                tempBolus.created_at = new Date(tempBolus.date);
+                tempBoluses.push(tempBolus);
+            }
+        }
+    }
+    var all_data =  [ ].concat(tempBoluses).concat(tempHistory);
+    all_data = _.sortBy(all_data, 'date');
+    return all_data;
+}
+exports = module.exports = calcTempTreatments;

+ 84 - 0
oref0/lib/lib/iob/index.js

@@ -0,0 +1,84 @@
+
+var tz = require('moment-timezone');
+var find_insulin = require('./history');
+var calculate = require('./calculate');
+var sum = require('./total');
+
+function generate (inputs, currentIOBOnly, treatments) {
+
+    if (!treatments) {
+        var treatments = find_insulin(inputs);
+        // calculate IOB based on continuous future zero temping as well
+        var treatmentsWithZeroTemp = find_insulin(inputs, 240);
+    } else {
+        var treatmentsWithZeroTemp = [];
+    }
+    //console.error(treatments.length, treatmentsWithZeroTemp.length);
+    //console.error(treatments[treatments.length-1], treatmentsWithZeroTemp[treatmentsWithZeroTemp.length-1])
+
+    var opts = {
+        treatments: treatments
+    , profile: inputs.profile
+    , calculate: calculate
+    };
+    if ( inputs.autosens ) {
+        opts.autosens = inputs.autosens;
+    }
+    var optsWithZeroTemp = {
+        treatments: treatmentsWithZeroTemp
+    , profile: inputs.profile
+    , calculate: calculate
+    };
+
+    var iobArray = [];
+    //console.error(inputs.clock);
+    if (! /(Z|[+-][0-2][0-9]:?[034][05])+/.test(inputs.clock) ) {
+        console.error("Warning: clock input " + inputs.clock + " is unzoned; please pass clock-zoned.json instead");
+    }
+    var clock = new Date(tz(inputs.clock));
+
+    var lastBolusTime = new Date(0).getTime(); //clock.getTime());
+    var lastTemp = {};
+    lastTemp.date = new Date(0).getTime(); //clock.getTime());
+    //console.error(treatments[treatments.length-1]);
+    treatments.forEach(function(treatment) {
+        if (treatment.insulin && treatment.started_at) {
+            lastBolusTime = Math.max(lastBolusTime,treatment.started_at);
+            //console.error(treatment.insulin,treatment.started_at,lastBolusTime);
+        } else if (typeof(treatment.rate) === 'number' && treatment.duration ) {
+            if ( treatment.date > lastTemp.date ) {
+                lastTemp = treatment;
+                lastTemp.duration = Math.round(lastTemp.duration*100)/100;
+            }
+
+            //console.error(treatment.rate, treatment.duration, treatment.started_at,lastTemp.started_at)
+        }
+        //console.error(treatment.rate, treatment.duration, treatment.started_at,lastTemp.started_at)
+        //if (treatment.insulin && treatment.started_at) { console.error(treatment.insulin,treatment.started_at,lastBolusTime); }
+    });
+    var iStop;
+    if (currentIOBOnly) {
+        // for COB calculation, we only need the zeroth element of iobArray
+        iStop=1
+    } else {
+        // predict IOB out to 4h, regardless of DIA
+        iStop=4*60;
+    }
+    for (var i=0; i<iStop; i+=5){
+        t = new Date(clock.getTime() + i*60000);
+        //console.error(t);
+        var iob = sum(opts, t);
+        var iobWithZeroTemp = sum(optsWithZeroTemp, t);
+        //console.error(opts.treatments[opts.treatments.length-1], optsWithZeroTemp.treatments[optsWithZeroTemp.treatments.length-1])
+        iobArray.push(iob);
+        //console.error(iob.iob, iobWithZeroTemp.iob);
+        //console.error(iobArray.length-1, iobArray[iobArray.length-1]);
+        iobArray[iobArray.length-1].iobWithZeroTemp = iobWithZeroTemp;
+    }
+    //console.error(lastBolusTime);
+    iobArray[0].lastBolusTime = lastBolusTime;
+    iobArray[0].lastTemp = lastTemp;
+    return iobArray;
+}
+
+exports = module.exports = generate;

+ 103 - 0
oref0/lib/lib/iob/total.js

@@ -0,0 +1,103 @@
+function iobTotal(opts, time) {
+
+    var now = time.getTime();
+    var iobCalc = opts.calculate;
+    var treatments = opts.treatments;
+    var profile_data = opts.profile;
+    var dia = profile_data.dia;
+    var peak = 0;
+    var iob = 0;
+    var basaliob = 0;
+    var bolusiob = 0;
+    var netbasalinsulin = 0;
+    var bolusinsulin = 0;
+    //var bolussnooze = 0;
+    var activity = 0;
+    if (!treatments) return {};
+    //if (typeof time === 'undefined') {
+        //var time = new Date();
+    //}
+
+    // force minimum DIA of 3h
+    if (dia < 3) {
+        //console.error("Warning; adjusting DIA from",dia,"to minimum of 3 hours");
+        dia = 3;
+    }
+
+    var curveDefaults = {
+        'bilinear': {
+            requireLongDia: false,
+            peak: 75 // not really used, but prevents having to check later
+        },
+        'rapid-acting': {
+            requireLongDia: true,
+            peak: 75,
+            tdMin: 300
+        },
+        'ultra-rapid': {
+            requireLongDia: true,
+            peak: 55,
+            tdMin: 300
+        },
+    };
+
+    var curve = 'bilinear';
+
+    if (profile_data.curve !== undefined) {
+        curve = profile_data.curve.toLowerCase();
+    }
+
+    if (!(curve in curveDefaults)) {
+        console.error('Unsupported curve function: "' + curve + '". Supported curves: "bilinear", "rapid-acting" (Novolog, Novorapid, Humalog, Apidra) and "ultra-rapid" (Fiasp). Defaulting to "rapid-acting".');
+        curve = 'rapid-acting';
+    }
+
+    var defaults = curveDefaults[curve];
+
+    // Force minimum of 5 hour DIA when default requires a Long DIA.
+    if (defaults.requireLongDia && dia < 5) {
+        //console.error('Pump DIA must be set to 5 hours or more with the new curves, please adjust your pump. Defaulting to 5 hour DIA.');
+        dia = 5;
+    }
+
+    peak = defaults.peak;
+
+    treatments.forEach(function(treatment) {
+        if( treatment.date <= now ) {
+            var dia_ago = now - dia*60*60*1000;
+            if( treatment.date > dia_ago ) {
+                // tIOB = total IOB
+                var tIOB = iobCalc(treatment, time, curve, dia, peak, profile_data);
+                if (tIOB && tIOB.iobContrib) { iob += tIOB.iobContrib; }
+                if (tIOB && tIOB.activityContrib) { activity += tIOB.activityContrib; }
+                // basals look like either of these:
+                // {"insulin":-0.05,"date":1507265512363.6365,"created_at":"2017-10-06T04:51:52.363Z"}
+                // {"insulin":0.05,"date":1507266530000,"created_at":"2017-10-06T05:08:50.000Z"}
+                // boluses look like:
+                // {"timestamp":"2017-10-05T22:06:31-07:00","started_at":"2017-10-06T05:06:31.000Z","date":1507266391000,"insulin":0.5}
+                if (treatment.insulin && tIOB && tIOB.iobContrib) {
+                    if (treatment.insulin < 0.1) {
+                        basaliob += tIOB.iobContrib;
+                        netbasalinsulin += treatment.insulin;
+                    } else {
+                        bolusiob += tIOB.iobContrib;
+                        bolusinsulin += treatment.insulin;
+                    }
+                }
+                //console.error(JSON.stringify(treatment));
+            }
+        } // else { console.error("ignoring future treatment:",treatment); }
+    });
+
+    return {
+        iob: Math.round(iob * 1000) / 1000,
+        activity: Math.round(activity * 10000) / 10000,
+        basaliob: Math.round(basaliob * 1000) / 1000,
+        bolusiob: Math.round(bolusiob * 1000) / 1000,
+        netbasalinsulin: Math.round(netbasalinsulin * 1000) / 1000,
+        bolusinsulin: Math.round(bolusinsulin * 1000) / 1000,
+        time: time
+    };
+}
+
+exports = module.exports = iobTotal;

+ 144 - 0
oref0/lib/lib/meal/history.js

@@ -0,0 +1,144 @@
+function arrayHasElementWithSameTimestampAndProperty(array,t,propname) {
+    for (var j=0; j < array.length; j++) {
+        var element = array[j];
+        if (element.timestamp === t && element[propname] !== undefined) return true;
+        if ( element[propname] !== undefined ) {
+            var eDate = new Date(element.timestamp);
+            var tDate = new Date(t);
+            var tMin = new Date(tDate.getTime() - 2000);
+            var tMax = new Date(tDate.getTime() + 2000);
+            //console.error(tDate, tMin, tMax);
+            if (eDate > tMin && eDate < tMax) return true;
+        }
+    }
+    return false;
+}
+
+function findMealInputs (inputs) {
+    var pumpHistory = inputs.history;
+    var carbHistory = inputs.carbs;
+    var profile_data = inputs.profile;
+    var mealInputs = [];
+    var bolusWizardInputs = [];
+    var duplicates = 0;
+
+    for (var i=0; i < carbHistory.length; i++) {
+        var current = carbHistory[i];
+        if (current.carbs && current.actualDate) {
+            var temp = {};
+            temp.timestamp = current.actualDate;
+            temp.carbs = current.carbs;
+            temp.nsCarbs = current.carbs;
+        } else if (current.carbs && current.created_at) {
+            var temp = {};
+            temp.timestamp = current.created_at;
+            temp.carbs = current.carbs;
+            temp.nsCarbs = current.carbs;
+        if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.created_at,"carbs") ||
+            !arrayHasElementWithSameTimestampAndProperty(mealInputs, current.actualDate,"carbs")) {
+                mealInputs.push(temp);
+            } else {
+                duplicates += 1;
+            }
+        }
+    }
+
+    for (i=0; i < pumpHistory.length; i++) {
+        current = pumpHistory[i];
+        if (current._type === "Bolus" && current.timestamp) {
+            //console.log(pumpHistory[i]);
+            temp = {};
+            temp.timestamp = current.timestamp;
+            temp.bolus = current.amount;
+
+            if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"bolus")) {
+                mealInputs.push(temp);
+            } else {
+                duplicates += 1;
+            }
+        } else if (current._type === "BolusWizard" && current.timestamp) {
+            // Delay process the BolusWizard entries to make sure we've seen all possible that correspond to the bolus wizard.
+            // More specifically, we need to make sure we process the corresponding bolus entry first.
+            bolusWizardInputs.push(current);
+
+        } else if ((current._type === "Meal Bolus" || current._type === "Correction Bolus" || current._type === "Snack Bolus" || current._type === "Bolus Wizard" || current._type === "Carb Correction") && current.created_at || current.actualDate) {            //imports carbs entered through Nightscout Care Portal
+            //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard
+            temp = {};
+            temp.timestamp = current.created_at;
+            temp.carbs = current.carbs;
+            temp.nsCarbs = current.carbs;
+            // don't enter the treatment if there's another treatment with the same exact timestamp
+            // to prevent duped carb entries from multiple sources
+            if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.created_at,"carbs")) {
+                mealInputs.push(temp);
+            } else {
+                duplicates += 1;
+            }
+        } else if (current.enteredBy === "xdrip") {
+            temp = {};
+            temp.timestamp = current.created_at;
+            temp.carbs = current.carbs;
+            temp.nsCarbs = current.carbs;
+            temp.bolus = current.insulin;
+            if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) {
+                mealInputs.push(temp);
+            } else {
+                duplicates += 1;
+            }
+        } else if (current.carbs > 0) {
+            temp = {};
+            temp.carbs = current.carbs;
+            temp.nsCarbs = current.carbs;
+            temp.timestamp = current.actualDate || current.created_at;
+            temp.bolus = current.insulin;
+            if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) {
+                mealInputs.push(temp);
+            } else {
+                duplicates += 1;
+            }
+        } else if (current._type === "JournalEntryMealMarker" && current.carb_input > 0 && current.timestamp) {
+            temp = {};
+            temp.timestamp = current.actualDate || current.created_at;
+            temp.carbs = current.carb_input;
+            temp.journalCarbs = current.carb_input;
+            if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) {
+                    mealInputs.push(temp);
+                } else {
+                    duplicates += 1;
+            }
+        }
+    }
+
+    for(i=0; i < bolusWizardInputs.length; i++) {
+      current = bolusWizardInputs[i];
+      //console.log(bolusWizardInputs[i]);
+      temp = {};
+      temp.timestamp = current.timestamp;
+      temp.carbs = current.carb_input;
+      temp.bwCarbs = current.carb_input;
+
+      // don't enter the treatment if there's another treatment with the same exact timestamp
+      // to prevent duped carb entries from multiple sources
+      if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) {
+          if (arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"bolus")) {
+              mealInputs.push(temp);
+              //bwCarbs += temp.carbs;
+          } else {
+              console.error("Skipping bolus wizard entry", i, "in the pump history with",current.carb_input,"g carbs and no insulin.");
+              if (current.carb_input === 0) {
+                console.error("This is caused by a BolusWizard without carbs. If you specified insulin, it will be noted as a seperate Bolus");
+              }
+              if (current.timestamp) {
+                  console.error("Timestamp of bolus wizard:", current.timestamp);
+              }
+          }
+      } else {
+          duplicates += 1;
+      }
+    }
+    //if (duplicates > 0) console.error("Removed duplicate bolus/carb entries:" + duplicates);
+
+    return mealInputs;
+}
+
+exports = module.exports = findMealInputs;

+ 23 - 0
oref0/lib/lib/meal/index.js

@@ -0,0 +1,23 @@
+
+var tz = require('moment-timezone');
+var find_meals = require('./history');
+var sum = require('./total');
+
+function generate (inputs) {
+
+  var treatments = find_meals(inputs);
+
+  var opts = {
+    treatments: treatments
+  , profile: inputs.profile
+  , pumphistory: inputs.history
+  , glucose: inputs.glucose
+  , basalprofile: inputs.basalprofile
+  };
+
+  var clock = new Date(tz(inputs.clock));
+
+  return /* meal_data */ sum(opts, clock);
+}
+
+exports = module.exports = generate;

+ 142 - 0
oref0/lib/lib/meal/total.js

@@ -0,0 +1,142 @@
+var tz = require('moment-timezone');
+var calcMealCOB = require('../determine-basal/cob');
+
+function recentCarbs(opts, time) {
+    var treatments = opts.treatments;
+    var profile_data = opts.profile;
+    if (typeof(opts.glucose) !== 'undefined') {
+        var glucose_data = opts.glucose;
+    }
+    var carbs = 0;
+    var nsCarbs = 0;
+    var bwCarbs = 0;
+    var journalCarbs = 0;
+    var bwFound = false;
+    var mealCarbTime = time.getTime();
+    var lastCarbTime = 0;
+    if (!treatments) return {};
+
+    //console.error(glucose_data);
+    var iob_inputs = {
+        profile: profile_data
+    ,   history: opts.pumphistory
+    };
+    var COB_inputs = {
+        glucose_data: glucose_data
+    ,   iob_inputs: iob_inputs
+    ,   basalprofile: opts.basalprofile
+    ,   mealTime: mealCarbTime
+    };
+    var mealCOB = 0;
+
+    // this sorts the treatments collection in order.
+    treatments.sort(function (a, b) {
+        var aDate = new Date(tz(a.timestamp));
+        var bDate = new Date(tz(b.timestamp));
+        //console.error(aDate);
+        return bDate.getTime() - aDate.getTime();
+    });
+
+    var carbsToRemove = 0;
+    var nsCarbsToRemove = 0;
+    var bwCarbsToRemove = 0;
+    var journalCarbsToRemove = 0;
+    treatments.forEach(function(treatment) {
+        var now = time.getTime();
+        // consider carbs from up to 6 hours ago in calculating COB
+        var carbWindow = now - 6 * 60*60*1000;
+        var treatmentDate = new Date(tz(treatment.timestamp));
+        var treatmentTime = treatmentDate.getTime();
+        if (treatmentTime > carbWindow && treatmentTime <= now) {
+            if (treatment.carbs >= 1) {
+                if (treatment.nsCarbs >= 1) {
+                    nsCarbs += parseFloat(treatment.nsCarbs);
+                } else if (treatment.bwCarbs >= 1) {
+                    bwCarbs += parseFloat(treatment.bwCarbs);
+                    bwFound = true;
+                } else if (treatment.journalCarbs >= 1) {
+                    journalCarbs += parseFloat(treatment.journalCarbs);
+                } else {
+                    console.error("Treatment carbs unclassified:",treatment);
+                }
+                //console.error(treatment.carbs, maxCarbs, treatmentDate);
+                carbs += parseFloat(treatment.carbs);
+                COB_inputs.mealTime = treatmentTime;
+                lastCarbTime = Math.max(lastCarbTime,treatmentTime);
+                var myCarbsAbsorbed = calcMealCOB(COB_inputs).carbsAbsorbed;
+                var myMealCOB = Math.max(0, carbs - myCarbsAbsorbed);
+                if (typeof(myMealCOB) === 'number' && ! isNaN(myMealCOB)) {
+                    mealCOB = Math.max(mealCOB, myMealCOB);
+                } else {
+                    console.error("Bad myMealCOB:",myMealCOB, "mealCOB:",mealCOB, "carbs:",carbs,"myCarbsAbsorbed:",myCarbsAbsorbed);
+                }
+                if (myMealCOB < mealCOB) {
+                    carbsToRemove += parseFloat(treatment.carbs);
+                    if (treatment.nsCarbs >= 1) {
+                        nsCarbsToRemove += parseFloat(treatment.nsCarbs);
+                    } else if (treatment.bwCarbs >= 1) {
+                        bwCarbsToRemove += parseFloat(treatment.bwCarbs);
+                    } else if (treatment.journalCarbs >= 1) {
+                        journalCarbsToRemove += parseFloat(treatment.journalCarbs);
+                    }
+                } else {
+                    carbsToRemove = 0;
+                    nsCarbsToRemove = 0;
+                    bwCarbsToRemove = 0;
+                }
+                //console.error(carbs, carbsToRemove);
+                //console.error("COB:",mealCOB);
+            }
+        }
+    });
+    // only include carbs actually used in calculating COB
+    carbs -= carbsToRemove;
+    nsCarbs -= nsCarbsToRemove;
+    bwCarbs -= bwCarbsToRemove;
+    journalCarbs -= journalCarbsToRemove;
+
+    // calculate the current deviation and steepest deviation downslope over the last hour
+    COB_inputs.ciTime = time.getTime();
+    // set mealTime to 6h ago for Deviation calculations
+    COB_inputs.mealTime = time.getTime() - 6 * 60 * 60 * 1000;
+    var c = calcMealCOB(COB_inputs);
+    //console.error(c.currentDeviation, c.slopeFromMaxDeviation);
+
+    // set a hard upper limit on COB to mitigate impact of erroneous or malicious carb entry
+    if (typeof(profile.maxCOB) === 'number' && ! isNaN(profile.maxCOB)) {
+        mealCOB = Math.min( profile.maxCOB, mealCOB );
+    } else {
+        console.error("Bad profile.maxCOB:",profile.maxCOB);
+    }
+
+    // if currentDeviation is null or maxDeviation is 0, set mealCOB to 0 for zombie-carb safety
+    if (typeof(c.currentDeviation) === 'undefined' || c.currentDeviation === null) {
+        console.error("");
+        console.error("Warning: setting mealCOB to 0 because currentDeviation is null/undefined");
+        mealCOB = 0;
+    }
+    if (typeof(c.maxDeviation) === 'undefined' || c.maxDeviation === null) {
+        console.error("");
+        console.error("Warning: setting mealCOB to 0 because maxDeviation is 0 or undefined");
+        mealCOB = 0;
+    }
+
+    return {
+        carbs: Math.round( carbs * 1000 ) / 1000
+    ,   nsCarbs: Math.round( nsCarbs * 1000 ) / 1000
+    ,   bwCarbs: Math.round( bwCarbs * 1000 ) / 1000
+    ,   journalCarbs: Math.round( journalCarbs * 1000 ) / 1000
+    ,   mealCOB: Math.round( mealCOB )
+    ,   currentDeviation: Math.round( c.currentDeviation * 100 ) / 100
+    ,   maxDeviation: Math.round( c.maxDeviation * 100 ) / 100
+    ,   minDeviation: Math.round( c.minDeviation * 100 ) / 100
+    ,   slopeFromMaxDeviation: Math.round( c.slopeFromMaxDeviation * 1000 ) / 1000
+    ,   slopeFromMinDeviation: Math.round( c.slopeFromMinDeviation * 1000 ) / 1000
+    ,   allDeviations: c.allDeviations
+    ,   lastCarbTime: lastCarbTime
+    ,   bwFound: bwFound
+    };
+}
+
+exports = module.exports = recentCarbs;
+

+ 12 - 0
oref0/lib/lib/medtronic-clock.js

@@ -0,0 +1,12 @@
+
+function getTime(minutes) {
+    var baseTime = new Date();
+    baseTime.setHours('00');
+    baseTime.setMinutes('00');
+    baseTime.setSeconds('00');    
+    
+    return baseTime.getTime() + minutes * 60 * 1000;
+}
+
+exports = module.exports = getTime;
+

+ 9 - 0
oref0/lib/lib/oref0-setup/alias.json

@@ -0,0 +1,9 @@
+[
+  {
+    "type": "alias",
+    "name": "invoke",
+    "invoke": {
+      "command": "report invoke"
+    }
+  }
+]

+ 36 - 0
oref0/lib/lib/oref0-setup/autotune.json

@@ -0,0 +1,36 @@
+[
+  {
+    "settings/pumpprofile.json": {
+      "use": "shell",
+      "bg_targets": "settings/bg_targets.json",
+      "preferences": "preferences.json",
+      "settings": "settings/settings.json",
+      "basal_profile": "settings/basal_profile.json",
+      "reporter": "text",
+      "json_default": "True",
+      "carb_ratios": "settings/carb_ratios.json",
+      "device": "get-profile",
+      "remainder": "--model=settings/model.json",
+      "isf": "settings/insulin_sensitivities.json"
+    },
+    "type": "report",
+    "name": "settings/pumpprofile.json"
+  },
+  {
+    "settings/profile.json": {
+      "use": "shell",
+      "bg_targets": "settings/bg_targets.json",
+      "preferences": "preferences.json",
+      "settings": "settings/settings.json",
+      "basal_profile": "settings/basal_profile.json",
+      "reporter": "text",
+      "json_default": "True",
+      "carb_ratios": "settings/carb_ratios.json",
+      "device": "get-profile",
+      "remainder": "settings/temptargets.json --model=settings/model.json --autotune settings/autotune.json",
+      "isf": "settings/insulin_sensitivities.json"
+    },
+    "type": "report",
+    "name": "settings/profile.json"
+  }
+]

+ 50 - 0
oref0/lib/lib/oref0-setup/basal_profile.json

@@ -0,0 +1,50 @@
+[
+  {
+    "i": 0,
+    "start": "00:00:00",
+    "rate": 0,
+    "minutes": 0
+  },
+  {
+    "i": 1,
+    "start": "02:30:00",
+    "rate": 0,
+    "minutes": 150
+  },
+  {
+    "i": 2,
+    "start": "06:00:00",
+    "rate": 0,
+    "minutes": 360
+  },
+  {
+    "i": 3,
+    "start": "09:00:00",
+    "rate": 0,
+    "minutes": 600
+  },
+  {
+    "i": 4,
+    "start": "11:30:00",
+    "rate": 0,
+    "minutes": 690
+  },
+  {
+    "i": 5,
+    "start": "14:00:00",
+    "rate": 0,
+    "minutes": 840
+  },
+  {
+    "i": 6,
+    "start": "18:30:00",
+    "rate": 0,
+    "minutes": 1110
+  },
+  {
+    "i": 7,
+    "start": "23:00:00",
+    "rate": 0,
+    "minutes": 1380
+  }
+]

+ 24 - 0
oref0/lib/lib/oref0-setup/bg_targets_raw.json

@@ -0,0 +1,24 @@
+{
+  "units": "mg/dL",
+  "targets": [
+    {
+      "high": 120,
+      "start": "00:00:00",
+      "low": 110,
+      "offset": 0
+    },
+    {
+      "high": 110,
+      "start": "06:00:00",
+      "low": 110,
+      "offset": 360
+    },
+    {
+      "high": 120,
+      "start": "20:00:00",
+      "low": 110,
+      "offset": 1200
+    }
+  ],
+  "first": 1
+}

+ 270 - 0
oref0/lib/lib/oref0-setup/cgm-loop.json

@@ -0,0 +1,270 @@
+[
+  {
+    "openapscontrib.timezones": {
+      "path": ".",
+      "module": "openapscontrib.timezones"
+    },
+    "type": "vendor",
+    "name": "openapscontrib.timezones"
+  },
+  {
+    "type": "device",
+    "tz": {
+      "vendor": "openapscontrib.timezones",
+      "extra": "tz.ini"
+    },
+    "name": "tz",
+    "extra": {}
+  },
+  {
+    "extra": {
+      "fields": "",
+      "cmd": "oref0",
+      "args": ""
+    },
+    "type": "device",
+    "name": "oref0",
+    "oref0": {
+      "vendor": "openaps.vendors.process",
+      "extra": "oref0.ini"
+    }
+  },
+  {
+    "raw": {
+      "vendor": "openaps.vendors.process",
+      "extra": "raw.ini"
+    },
+    "type": "device",
+    "name": "raw",
+    "extra": {
+      "fields": "glucose cal maxraw",
+      "cmd": "oref0",
+      "args": "raw"
+    }
+  },
+  {
+    "merge": {
+      "vendor": "openaps.vendors.process",
+      "extra": "merge.ini"
+    },
+    "type": "device",
+    "name": "merge",
+    "extra": {
+      "fields": "short long",
+      "cmd": "jq -s \".[0] + .[1]|unique|sort_by(.dateString)|reverse|.[0:288]\"",
+      "args": ""
+    }
+  },
+  {
+    "extra": {
+      "fields": "file",
+      "cmd": "jq .[0:2]",
+      "args": ""
+    },
+    "type": "device",
+    "name": "first",
+    "first": {
+      "vendor": "openaps.vendors.process",
+      "extra": "first.ini"
+    }
+  },
+  {
+    "type": "report",
+    "name": "monitor/cal.json",
+    "monitor/cal.json": {
+      "count": "1",
+      "use": "nightscout_calibrations",
+      "reporter": "JSON",
+      "seconds": "",
+      "hours": "",
+      "device": "cgm",
+      "gaps": "",
+      "microseconds": "",
+      "date": "display_time",
+      "minutes": ""
+    }
+  },
+  {
+    "monitor/cal-zoned.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "display_time system_time dateString",
+      "adjust": "missing",
+      "input": "monitor/cal.json",
+      "device": "tz",
+      "timezone": ""
+    },
+    "type": "report",
+    "name": "monitor/cal-zoned.json"
+  },
+  {
+    "monitor/glucose-oref0.json": {
+      "count": "",
+      "use": "oref0_glucose",
+      "no_raw": "False",
+      "reporter": "JSON",
+      "seconds": "",
+      "minutes": "30.0",
+      "hours": "",
+      "device": "cgm",
+      "gaps": "",
+      "microseconds": "",
+      "threshold": "100",
+      "sensor": "",
+      "date": "display_time",
+      "glucose": ""
+    },
+    "type": "report",
+    "name": "monitor/glucose-oref0.json"
+  },
+  {
+    "monitor/glucose-zoned.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "display_time system_time dateString",
+      "adjust": "missing",
+      "input": "monitor/glucose-oref0.json",
+      "device": "tz",
+      "timezone": ""
+    },
+    "type": "report",
+    "name": "monitor/glucose-zoned.json"
+  },
+  {
+    "monitor/glucose-raw.json": {
+      "use": "shell",
+      "reporter": "JSON",
+      "json_default": "True",
+      "cal": "monitor/cal.json",
+      "device": "raw",
+      "remainder": "",
+      "maxraw": "120",
+      "glucose": "monitor/glucose-zoned.json"
+    },
+    "type": "report",
+    "name": "monitor/glucose-raw.json"
+  },
+  {
+    "type": "report",
+    "nightscout/recent-missing-entries.json": {
+      "oper": "format-recent-type",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "tz entries monitor/glucose-zoned-merge.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/recent-missing-entries.json"
+  },
+  {
+    "type": "report",
+    "nightscout/uploaded-entries.json": {
+      "oper": "upload",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "entries.json nightscout/recent-missing-entries.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/uploaded-entries.json"
+  },
+  {
+    "nightscout/uploaded-cals.json": {
+      "oper": "upload",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "entries.json monitor/cal-zoned.json",
+      "json_default": "True"
+    },
+    "type": "report",
+    "name": "nightscout/uploaded-cals.json"
+  },
+  {
+    "monitor/glucose-oref0-long.json": {
+      "count": "",
+      "use": "oref0_glucose",
+      "no_raw": "False",
+      "reporter": "JSON",
+      "seconds": "",
+      "minutes": "",
+      "hours": "4.5",
+      "device": "cgm",
+      "gaps": "",
+      "microseconds": "",
+      "threshold": "100",
+      "sensor": "",
+      "date": "display_time",
+      "glucose": ""
+    },
+    "type": "report",
+    "name": "monitor/glucose-oref0-long.json"
+  },
+  {
+    "monitor/glucose-zoned-long.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "display_time system_time dateString",
+      "adjust": "missing",
+      "input": "monitor/glucose-oref0-long.json",
+      "device": "tz",
+      "timezone": ""
+    },
+    "type": "report",
+    "name": "monitor/glucose-zoned-long.json"
+  },
+  {
+    "monitor/glucose-zoned-merge.json": {
+      "use": "shell",
+      "short": "monitor/glucose-zoned.json",
+      "reporter": "JSON",
+      "device": "merge",
+      "remainder": "",
+      "json_default": "True",
+      "long": "monitor/glucose-zoned-merge.json"
+    },
+    "type": "report",
+    "name": "monitor/glucose-zoned-merge.json"
+  },
+  {
+    "type": "report",
+    "name": "monitor/glucose-raw-merge.json",
+    "monitor/glucose-raw-merge.json": {
+      "use": "shell",
+      "reporter": "JSON",
+      "json_default": "True",
+      "cal": "monitor/cal.json",
+      "device": "raw",
+      "remainder": "",
+      "maxraw": "200",
+      "glucose": "monitor/glucose-zoned-merge.json"
+    }
+  },
+  {
+    "monitor/glucose-zoned-first.json": {
+      "use": "shell",
+      "file": "monitor/glucose-zoned-merge.json",
+      "reporter": "JSON",
+      "device": "first",
+      "remainder": "",
+      "json_default": "True"
+    },
+    "type": "report",
+    "name": "monitor/glucose-zoned-first.json"
+  },
+  {
+    "nightscout/uploaded-first.json": {
+      "oper": "upload",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "entries.json monitor/glucose-zoned-first.json",
+      "json_default": "True"
+    },
+    "type": "report",
+    "name": "nightscout/uploaded-first.json"
+  }
+]

+ 131 - 0
oref0/lib/lib/oref0-setup/device.json

@@ -0,0 +1,131 @@
+[
+  {
+    "type": "device",
+    "cgm": {
+      "vendor": "openaps.vendors.dexcom",
+      "extra": "cgm.ini"
+    },
+    "name": "cgm",
+    "extra": {}
+  },
+  {
+    "extra": {
+      "fields": "",
+      "cmd": "oref0",
+      "args": ""
+    },
+    "type": "device",
+    "name": "oref0",
+    "oref0": {
+      "vendor": "openaps.vendors.process",
+      "extra": "oref0.ini"
+    }
+  },
+  {
+    "extra": {
+      "fields": "pumphistory profile clock",
+      "cmd": "oref0",
+      "args": "calculate-iob"
+    },
+    "type": "device",
+    "name": "iob",
+    "iob": {
+      "vendor": "openaps.vendors.process",
+      "extra": "iob.ini"
+    }
+  },
+  {
+    "extra": {
+      "fields": "pumphistory profile clock glucose basal carbs",
+      "cmd": "oref0",
+      "args": "meal"
+    },
+    "type": "device",
+    "name": "meal",
+    "meal": {
+      "vendor": "openaps.vendors.process",
+      "extra": "meal.ini"
+    }
+  },
+  {
+    "extra": {
+      "fields": "settings bg_targets isf basal_profile preferences carb_ratios",
+      "cmd": "oref0",
+      "args": "get-profile"
+    },
+    "type": "device",
+    "name": "get-profile",
+    "get-profile": {
+      "vendor": "openaps.vendors.process",
+      "extra": "get-profile.ini"
+    }
+  },
+  {
+    "extra": {
+      "fields": "glucose pumphistory isf basal_profile profile",
+      "cmd": "oref0",
+      "args": "detect-sensitivity"
+    },
+    "type": "device",
+    "name": "detect-sensitivity",
+    "detect-sensitivity": {
+      "vendor": "openaps.vendors.process",
+      "extra": "detect-sensitivity.ini"
+    }
+  },
+  {
+    "pebble": {
+      "vendor": "openaps.vendors.process",
+      "extra": "pebble.ini"
+    },
+    "type": "device",
+    "name": "pebble",
+    "extra": {
+      "fields": "glucose iob basal_profile temp_basal suggested enacted meal",
+      "cmd": "oref0",
+      "args": "pebble"
+    }
+  },
+  {
+    "type": "device",
+    "tz": {
+      "vendor": "openapscontrib.timezones",
+      "extra": "tz.ini"
+    },
+    "name": "tz",
+    "extra": {}
+  },
+  {
+    "pump": {
+      "vendor": "openaps.vendors.medtronic",
+      "extra": "pump.ini"
+    },
+    "type": "device",
+    "name": "pump",
+    "extra": {
+      "serial": "123456"
+    }
+  },
+  {
+    "units": {
+      "vendor": "openaps.vendors.units",
+      "extra": "units.ini"
+    },
+    "type": "device",
+    "name": "units",
+    "extra": {}
+  },
+  {
+    "xdrip": {
+      "vendor": "openaps.vendors.process",
+      "extra": "xdrip.ini"
+    },
+    "type": "device",
+    "name": "xdrip",
+    "extra": {
+      "fields": "",
+      "cmd": "bash -c \"curl --compressed -s http://localhost:5000/api/v1/entries?count=288 | json -e \\\"this.glucose = this.sgv\\\"\"",
+      "args": ""
+    }
+  }
+]

+ 11 - 0
oref0/lib/lib/oref0-setup/dexcom.json

@@ -0,0 +1,11 @@
+[
+  {
+    "type": "device",
+    "cgm": {
+      "vendor": "openaps.vendors.dexcom",
+      "extra": "cgm.ini"
+    },
+    "name": "cgm",
+    "extra": {}
+  }
+]

+ 2 - 0
oref0/lib/lib/oref0-setup/edisonbattery.json

@@ -0,0 +1,2 @@
+[
+]

+ 113 - 0
oref0/lib/lib/oref0-setup/mdt-cgm.json

@@ -0,0 +1,113 @@
+[
+  {
+    "type": "vendor",
+    "name": "openapscontrib.glucosetools",
+    "openapscontrib.glucosetools": {
+      "path": ".",
+      "module": "openapscontrib.glucosetools"
+    }
+  },
+  {
+    "extra": {},
+    "type": "device",
+    "name": "glucose",
+    "glucose": {
+      "vendor": "openapscontrib.glucosetools",
+      "extra": "glucose.ini"
+    }
+  },
+  {
+    "monitor/cgm-mm-glucosedirty.json": {
+      "hours": "24.0",
+      "device": "cgm",
+      "use": "iter_glucose_hours",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "monitor/cgm-mm-glucosedirty.json"
+  },
+  {
+    "type": "report",
+    "name": "cgm/cgm-glucose.json",
+    "cgm/cgm-glucose.json": {
+      "use": "clean",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "display_time dateString",
+      "adjust": "missing",
+      "input": "raw-cgm/raw-entries.json",
+      "device": "glucose",
+      "timezone": "",
+      "infile": "monitor/cgm-mm-glucosetrend.json"
+    }
+  },
+  {
+    "type": "report",
+    "name": "monitor/cgm-mm-glucosetrend.json",
+    "monitor/cgm-mm-glucosetrend.json": {
+      "device": "oref0",
+      "remainder": "mdt-trend monitor/cgm-mm-glucosedirty.json",
+      "use": "shell",
+      "json_default": "True",
+      "reporter": "JSON"
+    }
+  },
+  {
+    "type": "report",
+    "name": "monitor/glucose.json",
+    "monitor/glucose.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "timestamp dateString start_at end_at created_at display_time",
+      "adjust": "missing",
+      "input": "monitor/glucose-unzoned.json",
+      "device": "tz",
+      "timezone": ""
+    }
+  },
+  {
+    "type": "report",
+    "nightscout/recent-missing-entries.json": {
+      "oper": "format-recent-type",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "tz entries nightscout/glucose.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/recent-missing-entries.json"
+  },
+  {
+    "type": "report",
+    "nightscout/uploaded-entries.json": {
+      "oper": "upload",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "entries.json nightscout/recent-missing-entries.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/uploaded-entries.json"
+  },
+  {
+    "type": "report",
+    "nightscout/glucose.json": {
+      "oper": "mm-format-ns-glucose",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "monitor/glucose.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/glucose.json"
+  },
+  {
+    "//": "May be run directly by the user (mentioned in docs)",
+    "type": "alias",
+    "first-upload": {
+      "command": "! bash -c \"cat nightscout/glucose.json | json 1 > nightscout/recent-missing-entries.json && openaps report invoke nightscout/uploaded-entries.json\""
+    },
+    "name": "first-upload"
+  }
+]

+ 35 - 0
oref0/lib/lib/oref0-setup/pancreabble.json

@@ -0,0 +1,35 @@
+[
+{
+    "pancreabble": {
+      "path": ".",
+      "module": "pancreabble"
+    },
+    "type": "vendor",
+    "name": "pancreabble"
+  },
+  {
+    "type": "device",
+    "pbbl": {
+      "vendor": "pancreabble",
+      "extra": "pbbl.ini"
+    },
+    "name": "pbbl",
+    "extra": {
+      "port": "/dev/rfcomm0"
+    }    
+    },
+  {
+    "type": "report",
+    "name": "upload/urchin-data.json",
+    "upload/urchin-data.json": {
+      "use": "format_urchin_data",
+      "reporter": "JSON",
+      "cgm_clock": "monitor/clock-zoned.json",
+      "action": "add",
+      "device": "pbbl",
+      "glucose_history": "monitor/glucose.json",
+      "status_text": "",
+      "status_json": "upload/urchin-status.json"
+    }
+  }
+]

+ 7 - 0
oref0/lib/lib/oref0-setup/pancreoptions.json

@@ -0,0 +1,7 @@
+{
+	"urchin_loop_on": true,
+	"urchin_loop_status": false,	
+	"urchin_iob": true,
+	"urchin_temp_rate": false,
+	"notify_temp_basal": false
+}

+ 354 - 0
oref0/lib/lib/oref0-setup/report.json

@@ -0,0 +1,354 @@
+[
+ {
+    "type": "report",
+    "settings/temptargets.json": {
+      "oper": "temp_targets",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "-18hours",
+      "json_default": "True"
+    },
+    "name": "settings/temptargets.json"
+  },
+ {
+    "type": "report",
+    "monitor/carbhistory.json": {
+      "oper": "carb_history",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "-36hours",
+      "json_default": "True"
+    },
+    "name": "monitor/carbhistory.json"
+  },
+  {
+    "type": "report",
+    "name": "raw-cgm/raw-entries.json",
+    "raw-cgm/raw-entries.json": {
+      "count": "",
+      "use": "oref0_glucose",
+      "reporter": "JSON",
+      "seconds": "",
+      "minutes": "",
+      "hours": "24",
+      "device": "cgm",
+      "gaps": "",
+      "microseconds": "",
+      "threshold": "100",
+      "sensor": "",
+      "date": "display_time",
+      "glucose": ""
+    }
+  },
+  {
+    "type": "report",
+    "monitor/mmtune.json": {
+      "device": "pump",
+      "use": "mmtune",
+      "reporter": "JSON"
+    },
+    "name": "monitor/mmtune.json"
+  },
+  {
+    "type": "report",
+    "settings/model.json": {
+      "device": "pump",
+      "use": "model",
+      "reporter": "JSON"
+    },
+    "name": "settings/model.json"
+  },
+  {
+    "monitor/clock.json": {
+      "device": "pump",
+      "use": "read_clock",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "monitor/clock.json"
+  },
+  {
+    "type": "report",
+    "name": "cgm/cgm-glucose.json",
+    "cgm/cgm-glucose.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "display_time dateString",
+      "adjust": "missing",
+      "input": "raw-cgm/raw-entries.json",
+      "device": "tz",
+      "timezone": ""
+    }
+  },
+  {
+    "monitor/clock-zoned.json": {
+      "use": "clock",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "None",
+      "adjust": "missing",
+      "input": "monitor/clock.json",
+      "device": "tz",
+      "timezone": ""
+    },
+    "type": "report",
+    "name": "monitor/clock-zoned.json"
+  },
+  {
+    "type": "report",
+    "name": "monitor/temp_basal.json",
+    "monitor/temp_basal.json": {
+      "device": "pump",
+      "use": "read_temp_basal",
+      "reporter": "JSON"
+    }
+  },
+  {
+    "monitor/reservoir.json": {
+      "device": "pump",
+      "use": "reservoir",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "monitor/reservoir.json"
+  },
+  {
+    "monitor/battery.json": {
+      "device": "pump",
+      "use": "read_battery_status",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "monitor/battery.json"
+  },
+  {
+    "type": "report",
+    "name": "monitor/status.json",
+    "monitor/status.json": {
+      "device": "pump",
+      "use": "status",
+      "reporter": "JSON"
+    }
+  },
+  {
+    "type": "report",
+    "name": "monitor/pumphistory.json",
+    "monitor/pumphistory.json": {
+      "hours": "2.0",
+      "device": "pump",
+      "use": "iter_pump_hours",
+      "reporter": "JSON"
+    }
+  },
+  {
+    "settings/pumphistory-24h.json": {
+      "hours": "27.0",
+      "device": "pump",
+      "use": "iter_pump_hours",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "settings/pumphistory-24h.json"
+  },
+  {
+    "monitor/pumphistory-zoned.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "timestamp dateString start_at end_at created_at",
+      "adjust": "missing",
+      "input": "monitor/pumphistory.json",
+      "device": "tz",
+      "timezone": ""
+    },
+    "type": "report",
+    "name": "monitor/pumphistory-zoned.json"
+  },
+  {
+    "type": "report",
+    "name": "settings/pumphistory-24h-zoned.json",
+    "settings/pumphistory-24h-zoned.json": {
+      "use": "rezone",
+      "reporter": "JSON",
+      "astimezone": "False",
+      "date": "timestamp dateString start_at end_at created_at",
+      "adjust": "missing",
+      "input": "settings/pumphistory-24h.json",
+      "device": "tz",
+      "timezone": ""
+    }
+  },
+  {
+    "type": "report",
+    "name": "monitor/iob.json",
+    "monitor/iob.json": {
+      "profile": "settings/profile.json",
+      "use": "shell",
+      "clock": "monitor/clock-zoned.json",
+      "reporter": "text",
+      "json_default": "True",
+      "pumphistory": "monitor/pumphistory-merged.json",
+      "device": "iob",
+      "remainder": "settings/autosens.json"
+    }
+  },
+  {
+    "type": "report",
+    "name": "monitor/meal.json",
+    "monitor/meal.json": {
+      "profile": "settings/profile.json",
+      "glucose": "monitor/glucose.json",
+      "clock": "monitor/clock-zoned.json",
+      "reporter": "text",
+      "json_default": "True",
+      "use": "shell",
+      "pumphistory": "monitor/pumphistory-merged.json",
+      "carbs": "monitor/carbhistory.json",
+      "device": "meal",
+      "remainder": "",
+      "basal": "settings/basal_profile.json"
+    }
+  },
+  {
+    "type": "report",
+    "settings/autosens.json": {
+      "profile": "settings/profile.json",
+      "use": "shell",
+      "reporter": "text",
+      "basal_profile": "settings/basal_profile.json",
+      "json_default": "True",
+      "pumphistory": "settings/pumphistory-24h-zoned.json",
+      "device": "detect-sensitivity",
+      "remainder": "monitor/carbhistory.json settings/temptargets.json",
+      "isf": "settings/insulin_sensitivities.json",
+      "glucose": "monitor/glucose.json"
+    },
+    "name": "settings/autosens.json"
+  },
+  {
+    "type": "report",
+    "settings/bg_targets.json": {
+      "device": "units",
+      "to": "mg/dL",
+      "use": "bg_targets",
+      "input": "settings/bg_targets_raw.json",
+      "reporter": "JSON"
+    },
+    "name": "settings/bg_targets.json"
+  },
+  {
+    "settings/insulin_sensitivities.json": {
+      "device": "units",
+      "to": "mg/dL",
+      "use": "insulin_sensitivities",
+      "input": "settings/insulin_sensitivities_raw.json",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "settings/insulin_sensitivities.json"
+  },
+  {
+    "settings/carb_ratios.json": {
+      "device": "pump",
+      "use": "read_carb_ratios",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "settings/carb_ratios.json"
+  },
+  {
+    "settings/basal_profile.json": {
+      "device": "pump",
+      "use": "read_selected_basal_profile",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "settings/basal_profile.json"
+  },
+  {
+    "settings/settings.json": {
+      "device": "pump",
+      "use": "read_settings",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "settings/settings.json"
+  },
+  {
+    "settings/profile.json": {
+      "use": "shell",
+      "bg_targets": "settings/bg_targets.json",
+      "preferences": "preferences.json",
+      "settings": "settings/settings.json",
+      "basal_profile": "settings/basal_profile.json",
+      "reporter": "text",
+      "json_default": "True",
+      "carb_ratios": "settings/carb_ratios.json",
+      "device": "get-profile",
+      "remainder": "settings/temptargets.json --model=settings/model.json",
+      "isf": "settings/insulin_sensitivities.json"
+    },
+    "type": "report",
+    "name": "settings/profile.json"
+  },
+  {
+    "type": "report",
+    "enact/enacted.json": {
+      "device": "pump",
+      "input": "enact/suggested.json",
+      "use": "set_temp_basal",
+      "reporter": "JSON"
+    },
+    "name": "enact/enacted.json"
+  },
+  {
+    "type": "report",
+    "upload/pebble.json": {
+      "suggested": "enact/suggested.json",
+      "use": "shell",
+      "temp_basal": "monitor/temp_basal.json",
+      "reporter": "text",
+      "basal_profile": "settings/basal_profile.json",
+      "json_default": "True",
+      "meal": "monitor/meal.json",
+      "device": "pebble",
+      "enacted": "enact/enacted.json",
+      "remainder": "",
+      "iob": "monitor/iob.json",
+      "glucose": "monitor/glucose.json"
+    },
+    "name": "upload/pebble.json"
+  },
+  {
+    "type": "report",
+    "name": "settings/bg_targets_raw.json",
+    "settings/bg_targets_raw.json": {
+      "device": "pump",
+      "use": "read_bg_targets",
+      "reporter": "JSON"
+    }
+  },
+  {
+    "settings/insulin_sensitivities_raw.json": {
+      "device": "pump",
+      "use": "read_insulin_sensitivities",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "settings/insulin_sensitivities_raw.json"
+  },
+  {
+    "type": "report",
+    "name": "xdrip/glucose.json",
+    "xdrip/glucose.json": {
+        "device": "xdrip",
+        "remainder": "",
+        "use": "shell",
+	    "json_default": "True",
+        "reporter": "text"
+    }
+  }
+]

+ 8 - 0
oref0/lib/lib/oref0-setup/settings.json

@@ -0,0 +1,8 @@
+{
+  "maxBasal": 1.5,
+  "temp_basal": {
+    "percent": 100,
+    "type": "Units/hour"
+  }, 
+  "insulin_action_curve": 6
+}

+ 19 - 0
oref0/lib/lib/oref0-setup/shareble.json

@@ -0,0 +1,19 @@
+[
+  {
+    "type": "vendor",
+    "name": "openxshareble",
+    "openxshareble": {
+      "path": ".",
+      "module": "openxshareble"
+    }
+  },
+  {
+    "type": "device",
+    "cgm": {
+      "vendor": "openxshareble",
+      "extra": "cgm.ini"
+    },
+    "name": "cgm",
+    "extra": {}
+  }
+]

+ 22 - 0
oref0/lib/lib/oref0-setup/supermicrobolus.json

@@ -0,0 +1,22 @@
+[
+  {
+    "enact/smb-enacted.json": {
+      "device": "pump",
+      "input": "enact/smb-suggested.json",
+      "use": "set_temp_basal",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "enact/smb-enacted.json"
+  },
+  {
+    "enact/bolused.json": {
+      "device": "pump",
+      "input": "enact/smb-suggested.json",
+      "use": "bolus",
+      "reporter": "JSON"
+    },
+    "type": "report",
+    "name": "enact/bolused.json"
+  }
+]

+ 18 - 0
oref0/lib/lib/oref0-setup/vendor.json

@@ -0,0 +1,18 @@
+[
+  {
+    "openapscontrib.timezones": {
+      "path": ".",
+      "module": "openapscontrib.timezones"
+    },
+    "type": "vendor",
+    "name": "openapscontrib.timezones"
+  },
+  {
+    "type": "vendor",
+    "name": "mmeowlink.vendors.mmeowlink",
+    "mmeowlink.vendors.mmeowlink": {
+      "path": ".",
+      "module": "mmeowlink.vendors.mmeowlink"
+    }
+  }
+]

+ 26 - 0
oref0/lib/lib/oref0-setup/xdrip-cgm.json

@@ -0,0 +1,26 @@
+[
+  {
+    "type": "report",
+    "nightscout/recent-missing-entries.json": {
+      "oper": "format-recent-type",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "tz entries monitor/glucose.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/recent-missing-entries.json"
+  },
+  {
+    "type": "report",
+    "nightscout/uploaded-entries.json": {
+      "oper": "upload",
+      "use": "shell",
+      "reporter": "JSON",
+      "device": "ns",
+      "remainder": "entries.json nightscout/recent-missing-entries.json",
+      "json_default": "True"
+    },
+    "name": "nightscout/uploaded-entries.json"
+  }
+]

+ 17 - 0
oref0/lib/lib/percentile.js

@@ -0,0 +1,17 @@
+// From https://gist.github.com/IceCreamYou/6ffa1b18c4c8f6aeaad2
+// Returns the value at a given percentile in a sorted numeric array.
+// "Linear interpolation between closest ranks" method
+module.exports = function percentile(arr, p) {
+    if (arr.length === 0) return 0;
+    if (typeof p !== 'number') throw new TypeError('p must be a number');
+    if (p <= 0) return arr[0];
+    if (p >= 1) return arr[arr.length - 1];
+
+    var index = arr.length * p,
+        lower = Math.floor(index),
+        upper = lower + 1,
+        weight = index % 1;
+
+    if (upper >= arr.length) return arr[lower];
+    return arr[lower] * (1 - weight) + arr[upper] * weight;
+}

+ 45 - 0
oref0/lib/lib/profile/basal.js

@@ -0,0 +1,45 @@
+
+var _ = require('lodash');
+
+/* Return basal rate(U / hr) at the provided timeOfDay */
+function basalLookup (schedules, now) {
+
+    var nowDate = now;
+
+    if (typeof(now) === 'undefined') {
+      nowDate = new Date();
+    }
+
+    var basalprofile_data = _.sortBy(schedules, function(o) { return o.i; });
+    var basalRate = basalprofile_data[basalprofile_data.length-1].rate
+    if (basalRate === 0) {
+        console.error("ERROR: bad basal schedule",schedules);
+        return;
+    }
+    var nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes();
+
+    for (var i = 0; i < basalprofile_data.length - 1; i++) {
+        if ((nowMinutes >= basalprofile_data[i].minutes) && (nowMinutes < basalprofile_data[i + 1].minutes)) {
+            basalRate = basalprofile_data[i].rate;
+            break;
+        }
+    }
+    return Math.round(basalRate*1000)/1000;
+}
+
+
+function maxDailyBasal (inputs) {
+    var maxRate = _.maxBy(inputs.basals,function(o) { return Number(o.rate); });
+    return (Number(maxRate.rate) *1000)/1000;
+}
+
+/*Return maximum daily basal rate(U / hr) from profile.basals */
+
+function maxBasalLookup (inputs) {
+    return inputs.settings.maxBasal;
+}
+
+
+exports.maxDailyBasal = maxDailyBasal;
+exports.maxBasalLookup = maxBasalLookup;
+exports.basalLookup = basalLookup;

+ 38 - 0
oref0/lib/lib/profile/carbs.js

@@ -0,0 +1,38 @@
+
+var getTime = require('../medtronic-clock');
+
+function carbRatioLookup (inputs, profile) {
+    var now = new Date();
+    var carbratio_data = inputs.carbratio;
+    if (typeof(carbratio_data) !== "undefined" && typeof(carbratio_data.schedule) !== "undefined") {
+        var carbRatio;
+        if ((carbratio_data.units === "grams") || (carbratio_data.units === "exchanges")) {
+            //carbratio_data.schedule.sort(function (a, b) { return a.offset > b.offset });
+            carbRatio = carbratio_data.schedule[carbratio_data.schedule.length - 1];
+
+            for (var i = 0; i < carbratio_data.schedule.length - 1; i++) {
+                if ((now >= getTime(carbratio_data.schedule[i].offset)) && (now < getTime(carbratio_data.schedule[i + 1].offset))) {
+                    carbRatio = carbratio_data.schedule[i];
+                    // disallow impossibly high/low carbRatios due to bad decoding
+                    if (carbRatio < 3 || carbRatio > 150) {
+                        console.error("Error: carbRatio of " + carbRatio + " out of bounds.");
+                        return;
+                    }
+                    break;
+                }
+            }
+            if (carbratio_data.units === "exchanges") {
+                carbRatio.ratio = 12 / carbRatio.ratio
+            }
+            return carbRatio.ratio;
+        } else {
+            console.error("Error: Unsupported carb_ratio units " + carbratio_data.units);
+            return;
+        }
+    //return carbRatio.ratio;
+    //profile.carbratio = carbRatio.ratio;
+    } else { return; }
+}
+
+carbRatioLookup.carbRatioLookup = carbRatioLookup;
+exports = module.exports = carbRatioLookup;

+ 198 - 0
oref0/lib/lib/profile/index.js

@@ -0,0 +1,198 @@
+
+
+var basal = require('./basal');
+var targets = require('./targets');
+var isf = require('./isf');
+var carb_ratios = require('./carbs');
+var _ = require('lodash');
+
+function defaults ( ) {
+  return /* profile */ {
+    max_iob: 9 // if max_iob is not provided, will default to zero
+    , max_daily_safety_multiplier: 5
+    , current_basal_safety_multiplier: 6
+    , autosens_max: 2.5
+    , autosens_min: 0.5
+    , rewind_resets_autosens: true // reset autosensitivity to neutral for awhile after each pump rewind
+    // , autosens_adjust_targets: false // when autosens detects sensitivity/resistance, also adjust BG target accordingly
+    , high_temptarget_raises_sensitivity: false // raise sensitivity for temptargets >= 101.  synonym for exercise_mode
+    , low_temptarget_lowers_sensitivity: false // lower sensitivity for temptargets <= 99.
+    , sensitivity_raises_target: false // raise BG target when autosens detects sensitivity
+    , resistance_lowers_target: false // lower BG target when autosens detects resistance
+    , exercise_mode: false // when true, > 100 mg/dL high temp target adjusts sensitivityRatio for exercise_mode. This majorly changes the behavior of high temp targets from before. synonmym for high_temptarget_raises_sensitivity
+    , half_basal_exercise_target: 160 // when temptarget is 150 mg/dL *and* exercise_mode=true, run 50% basal at this level (120 = 75%; 140 = 60%)
+    // create maxCOB and default it to 120 because that's the most a typical body can absorb over 4 hours.
+    // (If someone enters more carbs or stacks more; OpenAPS will just truncate dosing based on 120.
+    // Essentially, this just limits AMA/SMB as a safety cap against excessive COB entry)
+    , maxCOB: 120
+    , skip_neutral_temps: false // if true, don't set neutral temps
+    , unsuspend_if_no_temp: false // if true, pump will un-suspend after a zero temp finishes
+    , min_5m_carbimpact: 8 // mg/dL per 5m (8 mg/dL/5m corresponds to 24g/hr at a CSF of 4 mg/dL/g (x/5*60/4))
+    , autotune_isf_adjustmentFraction: 1.0 // keep autotune ISF closer to pump ISF via a weighted average of fullNewISF and pumpISF.  1.0 allows full adjustment, 0 is no adjustment from pump ISF.
+    , remainingCarbsFraction: 1.0 // fraction of carbs we'll assume will absorb over 4h if we don't yet see carb absorption
+    , remainingCarbsCap: 90 // max carbs we'll assume will absorb over 4h if we don't yet see carb absorption
+    // WARNING: use SMB with caution: it can and will automatically bolus up to max_iob worth of extra insulin
+    , enableUAM: true // enable detection of unannounced meal carb absorption
+    , A52_risk_enable: false
+    , enableSMB_with_COB: true // enable supermicrobolus while COB is positive
+    , enableSMB_with_temptarget: true // enable supermicrobolus for eating soon temp targets
+    // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar
+    // LimiTTer, etc. do not properly filter out high-noise SGVs.  xDrip+ builds greater than or equal to
+    // version number d8e-7097-2018-01-22 provide proper noise values, so that oref0 can ignore high noise
+    // readings, and can temporarily raise the BG target when sensor readings have medium noise,
+    // resulting in appropriate SMB behaviour.  Older versions of xDrip+ should not be used with enableSMB_always.
+    // Using SMB overnight with such data sources risks causing a dangerous overdose of insulin
+    // if the CGM sensor reads falsely high and doesn't come down as actual BG does
+    , enableSMB_always: false // always enable supermicrobolus (unless disabled by high temptarget)
+    , enableSMB_after_carbs: false // enable supermicrobolus for 6h after carbs, even with 0 COB
+    // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar.
+    , allowSMB_with_high_temptarget: true // allow supermicrobolus (if otherwise enabled) even with high temp targets
+    , maxSMBBasalMinutes: 90 // maximum minutes of basal that can be delivered as a single SMB with uncovered COB
+    , maxUAMSMBBasalMinutes: 90 // maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB
+    , SMBInterval: 3 // minimum interval between SMBs, in minutes.
+    , bolus_increment: 0.05 // minimum bolus that can be delivered as an SMB
+    , maxDelta_bg_threshold: 0.2 // maximum change in bg to use SMB, above that will disable SMB
+    , curve: "rapid-acting" // change this to "ultra-rapid" for Fiasp, or "bilinear" for old curve
+    , useCustomPeakTime: false // allows changing insulinPeakTime
+    , insulinPeakTime: 45 // number of minutes after a bolus activity peaks.  defaults to 55m for Fiasp if useCustomPeakTime: false
+    , carbsReqThreshold: 1 // grams of carbsReq to trigger a pushover
+    , offline_hotspot: false // enabled an offline-only local wifi hotspot if no Internet available
+    , noisyCGMTargetMultiplier: 1.3 // increase target by this amount when looping off raw/noisy CGM data
+    , suspend_zeros_iob: true // recognize pump suspends as non insulin delivery events
+    // send the glucose data to the pump emulating an enlite sensor. This allows to setup high / low warnings when offline and see trend.
+    // To enable this feature, enable the sensor, set a sensor with id 0000000, go to start sensor and press find lost sensor.
+    , enableEnliteBgproxy: false
+    // TODO: make maxRaw a preference here usable by oref0-raw in myopenaps-cgm-loop
+    //, maxRaw: 200 // highest raw/noisy CGM value considered safe to use for looping
+    , calc_glucose_noise: false
+    , target_bg: false // set to an integer value in mg/dL to override pump min_bg
+    // autoISF variables
+    , smb_delivery_ratio: 0.5 //Default value: 0.5 Used if flexible delivery ratio is not used. This is another key OpenAPS safety cap, and specifies what share of the total insulin required can be delivered as SMB. This is to prevent people from getting into dangerous territory by setting SMB requests from the caregivers phone at the same time. Increase this experimental value slowly and with caution.
+    , adjustmentFactor: 1
+    , useNewFormula: false
+    , enableDynamicCR: false
+    , sigmoid: false
+    , weightPercentage: 0.65 
+    , tddAdjBasal: false // Enable adjustment of basal based on the ratio of 24 h : 7 day average TDD
+    , enableSMB_high_bg: false // enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile)
+    , enableSMB_high_bg_target: 110 // set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable.
+    , threshold_setting: 0.60 // Use a configurable threshold setting
+  }
+}
+
+function displayedDefaults () {
+    var allDefaults = defaults();
+    var profile = { };
+
+    profile.max_iob = allDefaults.max_iob;
+    profile.max_daily_safety_multiplier = allDefaults.max_daily_safety_multiplier;
+    profile.current_basal_safety_multiplier= allDefaults.current_basal_safety_multiplier;
+    profile.autosens_max = allDefaults.autosens_max;
+    profile.autosens_min = allDefaults.autosens_min;
+    profile.rewind_resets_autosens = allDefaults.rewind_resets_autosens;
+    profile.exercise_mode = allDefaults.exercise_mode;
+    profile.sensitivity_raises_target = allDefaults.sensitivity_raises_target;
+    profile.unsuspend_if_no_temp = allDefaults.unsuspend_if_no_temp;
+    profile.enableSMB_with_COB = allDefaults.enableSMB_with_COB;
+    profile.enableSMB_with_temptarget = allDefaults.enableSMB_with_temptarget;
+    profile.enableUAM = allDefaults.enableUAM;
+    profile.curve = allDefaults.curve;
+    profile.offline_hotspot = allDefaults.offline_hotspot;
+    profile.bolus_increment = allDefaults.bolus_increment;
+    profile.smb_delivery_ratio = allDefaults.smb_delivery_ratio;
+    profile.maxDelta_bg_threshold = allDefaults.maxDelta_bg_threshold;
+    profile.adjustmentFactor = allDefaults.adjustmentFactor;
+    profile.useNewFormula = allDefaults.useNewFormula;
+    profile.enableDynamicCR = allDefaults.enableDynamicCR;
+    profile.sigmoid = allDefaults.sigmoid;
+    profile.weightPercentage = allDefaults.weightPercentage;
+    profile.tddAdjBasal = allDefaults.tddAdjBasal;
+    profile.threshold_setting = allDefaults.threshold_setting;
+    profile.enableSMB_high_bg = allDefaults.enableSMB_high_bg;
+    profile.enableSMB_high_bg_target = allDefaults.enableSMB_high_bg_target;
+
+    console.error(profile);
+    return profile
+}
+
+function generate (inputs, opts) {
+  var profile = opts && opts.type ? opts : defaults( );
+
+  // check if inputs has overrides for any of the default prefs
+  // and apply if applicable
+  for (var pref in profile) {
+    if (inputs.hasOwnProperty(pref)) {
+      profile[pref] = inputs[pref];
+    }
+  }
+
+  var pumpsettings_data = inputs.settings;
+  if (inputs.settings.insulin_action_curve > 1) {
+    profile.dia =  pumpsettings_data.insulin_action_curve;
+  } else {
+    console.error('DIA of', profile.dia, 'is not supported');
+    return -1;
+  }
+
+  if (inputs.model) {
+    profile.model = inputs.model;
+  }
+  profile.skip_neutral_temps = inputs.skip_neutral_temps;
+
+  profile.current_basal = basal.basalLookup(inputs.basals);
+  profile.basalprofile = inputs.basals;
+
+  _.forEach(profile.basalprofile, function(basalentry) {
+    basalentry.rate = +(Math.round(basalentry.rate + "e+3")  + "e-3");
+  });
+
+  profile.max_daily_basal = basal.maxDailyBasal(inputs);
+  profile.max_basal = basal.maxBasalLookup(inputs);
+  if (profile.current_basal === 0) {
+    console.error("current_basal of",profile.current_basal,"is not supported");
+    return -1;
+  }
+  if (profile.max_daily_basal === 0) {
+    console.error("max_daily_basal of",profile.max_daily_basal,"is not supported");
+    return -1;
+  }
+  if (profile.max_basal < 0.1) {
+    console.error("max_basal of",profile.max_basal,"is not supported");
+    return -1;
+  }
+
+  var range = targets.bgTargetsLookup(inputs, profile);
+  profile.out_units = inputs.targets.user_preferred_units;
+  profile.min_bg = Math.round(range.min_bg);
+  profile.max_bg = Math.round(range.max_bg);
+  profile.bg_targets = inputs.targets;
+
+  _.forEach(profile.bg_targets.targets, function(bg_entry) {
+    bg_entry.high = Math.round(bg_entry.high);
+    bg_entry.low = Math.round(bg_entry.low);
+    bg_entry.min_bg = Math.round(bg_entry.min_bg);
+    bg_entry.max_bg = Math.round(bg_entry.max_bg);
+  });
+
+  delete profile.bg_targets.raw;
+
+  profile.temptargetSet = range.temptargetSet;
+  profile.sens = isf.isfLookup(inputs.isf);
+  profile.isfProfile = inputs.isf;
+  if (profile.sens < 5) {
+    console.error("ISF of",profile.sens,"is not supported");
+    return -1;
+  }
+  if (typeof(inputs.carbratio) !== "undefined") {
+    profile.carb_ratio = carb_ratios.carbRatioLookup(inputs, profile);
+    profile.carb_ratios = inputs.carbratio;
+  } else {
+    console.error("Profile wasn't given carb ratio data, cannot calculate carb_ratio");
+  }
+  return profile;
+}
+
+
+generate.defaults = defaults;
+generate.displayedDefaults = displayedDefaults;
+exports = module.exports = generate;

+ 47 - 0
oref0/lib/lib/profile/isf.js

@@ -0,0 +1,47 @@
+
+var _ = require('lodash');
+
+var lastResult = null;
+
+function isfLookup(isf_data, timestamp) {
+
+    var nowDate = timestamp;
+
+    if (typeof(timestamp) === 'undefined') {
+        nowDate = new Date();
+    }
+
+    var nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes();
+
+    if (lastResult && nowMinutes >= lastResult.offset && nowMinutes < lastResult.endOffset) {
+        return lastResult.sensitivity;
+    }
+
+    isf_data = _.sortBy(isf_data.sensitivities, function(o) { return o.offset; });
+
+    var isfSchedule = isf_data[isf_data.length - 1];
+
+    if (isf_data[0].offset !== 0) {
+        return -1;
+    }
+
+    var endMinutes = 1440;
+
+    for (var i = 0; i < isf_data.length - 1; i++) {
+        var currentISF = isf_data[i];
+        var nextISF = isf_data[i+1];
+        if (nowMinutes >= currentISF.offset && nowMinutes < nextISF.offset) {
+            endMinutes = nextISF.offset;
+            isfSchedule = isf_data[i];
+            break;
+        }
+    }
+
+    lastResult = isfSchedule;
+    lastResult.endOffset = endMinutes;
+
+    return isfSchedule.sensitivity;
+}
+
+isfLookup.isfLookup = isfLookup;
+exports = module.exports = isfLookup;

+ 84 - 0
oref0/lib/lib/profile/targets.js

@@ -0,0 +1,84 @@
+
+var getTime = require('../medtronic-clock');
+
+function bgTargetsLookup (inputs, profile) {
+  return bound_target_range(lookup(inputs, profile));
+}
+
+function lookup (inputs, profile) {
+    var bgtargets_data = inputs.targets;
+    var temptargets_data = inputs.temptargets;
+    var now = new Date();
+    
+    //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset });
+
+    var bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1];
+    
+    for (var i = 0; i < bgtargets_data.targets.length - 1; i++) {
+        if ((now >= getTime(bgtargets_data.targets[i].offset)) && (now < getTime(bgtargets_data.targets[i + 1].offset))) {
+            bgTargets = bgtargets_data.targets[i];
+            break;
+        }
+    }
+
+    if (profile.target_bg) {
+        bgTargets.low = profile.target_bg;
+    }
+
+    bgTargets.high = bgTargets.low;
+
+    var tempTargets = bgTargets;
+
+    // sort tempTargets by date so we can process most recent first
+    try {
+        temptargets_data.sort(function (a, b) { return new Date(b.created_at) - new Date(a.created_at) });
+    } catch (e) {
+        console.error("No temptargets found.");
+    }
+    //console.error(temptargets_data);
+    //console.error(now);
+    for (i = 0; i < temptargets_data.length; i++) {
+        var start = new Date(temptargets_data[i].created_at);
+        //console.error(start);
+        var expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000);
+        //console.error(expires);
+        if (now >= start && temptargets_data[i].duration === 0) {
+            // cancel temp targets
+            //console.error(temptargets_data[i]);
+            tempTargets = bgTargets;
+            break;
+        } else if (! temptargets_data[i].targetBottom || ! temptargets_data[i].targetTop) {
+            console.error("eventualBG target range invalid: " + temptargets_data[i].targetBottom + "-" + temptargets_data[i].targetTop);
+            break;
+        } else if (now >= start && now < expires ) {
+            //console.error(temptargets_data[i]);
+            tempTargets.high = temptargets_data[i].targetTop;
+            tempTargets.low = temptargets_data[i].targetBottom;
+            tempTargets.temptargetSet = true;
+            break;
+        }
+    }
+    bgTargets = tempTargets;
+    //console.error(bgTargets);
+
+    return bgTargets;
+}
+
+function bound_target_range (target) {
+    // if targets are < 20, assume for safety that they're intended to be mmol/L, and convert to mg/dL
+    if ( target.high < 20 ) { target.high = target.high * 18; }
+    if ( target.low < 20 ) { target.low = target.low * 18; }
+    // hard-code lower bounds for min_bg and max_bg in case pump is set too low, or units are wrong
+    target.max_bg = Math.max(80, target.high);
+    target.min_bg = Math.max(80, target.low);
+    // hard-code upper bound for min_bg in case pump is set too high
+    target.min_bg = Math.min(200, target.min_bg);
+    target.max_bg = Math.min(200, target.max_bg);
+    return target
+}
+
+bgTargetsLookup.bgTargetsLookup = bgTargetsLookup;
+bgTargetsLookup.lookup = lookup;
+bgTargetsLookup.bound_target_range = bound_target_range;
+exports = module.exports = bgTargetsLookup;
+

+ 35 - 0
oref0/lib/lib/pump.js

@@ -0,0 +1,35 @@
+
+function translate (treatments) {
+
+  var results = [ ];
+  
+  function step (current) {
+    var invalid = false;
+    switch (current._type) {
+      case 'CalBGForPH':
+        current.eventType = 'BG Check';
+        current.glucose = current.amount;
+        current.glucoseType = 'Finger';
+        break;
+      case 'BasalProfileStart':
+      case 'ResultDailyTotal':
+      case 'BGReceived':
+      case 'Sara6E':
+      case 'Model522ResultTotals':
+      case 'Model722ResultTotals':
+        invalid = true;
+        break;
+      default:
+        break;
+    }
+
+    if (!invalid) {
+      results.push(current);
+    }
+
+  }
+  treatments.forEach(step);
+  return results;
+}
+
+exports = module.exports = translate;

+ 81 - 0
oref0/lib/lib/require-utils.js

@@ -0,0 +1,81 @@
+'use strict';
+
+var fs = require('fs');
+
+function safeRequire (path) {
+  var resolved;
+
+  try {
+    resolved = require(path);
+  } catch (e) {
+    console.error("Could not require: " + path, e);
+  }
+
+  return resolved;
+}
+
+function safeLoadFile(path) {
+  
+  var resolved;
+
+  try {
+    resolved = JSON.parse(fs.readFileSync(path, 'utf8'));
+    //console.log('content = ' , resolved);
+  } catch (e) {
+    console.error("Could not require: " + path, e);
+  }
+  return resolved;
+}
+
+function requireWithTimestamp (path) {
+  var resolved = safeLoadFile(path);
+
+  if (resolved) {
+    resolved.timestamp = fs.statSync(path).mtime;
+  }
+  return resolved;
+}
+
+// Functions that are needed in order to test the module. Can be removed in the future.
+
+function compareMethods(path) {
+  var new_data = safeLoadFile(path);
+  var old_data = safeRequire(path);
+  if (JSON.stringify(new_data) === JSON.stringify(old_data) ) {
+    console.log("test passed", new_data, old_data); 
+  } else {
+    console.log("test failed"); 
+  }
+}
+
+// Module tests.
+if (!module.parent) {
+ // Write the first file: and test it.
+ var obj = {x: "x", y: 1}
+ fs.writeFileSync('/tmp/file1.json', JSON.stringify(obj));
+ compareMethods('/tmp/file1.json');
+
+  // Check a non existing object.
+  compareMethods('/tmp/not_exist.json');
+  
+  // check a file that is not formated well.
+  fs.writeFileSync('/tmp/bad.json', '{"x":"x","y":1');
+  compareMethods('/tmp/bad.json');
+
+  // Rewrite the file and reread it.
+  var new_obj = {x: "x", y: 2}
+  fs.writeFileSync('/tmp/file1.json', JSON.stringify(new_obj));
+  var obj_read = safeLoadFile('/tmp/file1.json');
+  if (JSON.stringify(new_obj) === JSON.stringify(obj_read) ) {
+    console.log("test passed"); 
+  } else {
+    console.log("test failed"); 
+  }
+
+}
+
+module.exports = {
+  safeRequire: safeRequire
+  , requireWithTimestamp: requireWithTimestamp
+  , safeLoadFile: safeLoadFile
+};

+ 44 - 0
oref0/lib/lib/round-basal.js

@@ -0,0 +1,44 @@
+var endsWith = require('lodash/endsWith');
+
+var round_basal = function round_basal(basal, profile) {
+
+    /* x23 and x54 pumps change basal increment depending on how much basal is being delivered:
+            0.025u for 0.025 < x < 0.975
+            0.05u for 1 < x < 9.95
+            0.1u for 10 < x
+      To round numbers nicely for the pump, use a scale factor of (1 / increment). */
+
+    var lowest_rate_scale = 20;
+
+    // Has profile even been passed in?
+    if (typeof profile !== 'undefined')
+    {
+        // Make sure optional model has been set
+        if (typeof profile.model === 'string')
+        {
+            if (endsWith(profile.model, "54") || endsWith(profile.model, "23"))
+            {
+                lowest_rate_scale = 40;
+            }
+        }
+    }
+
+    var rounded_basal = basal;
+    // Shouldn't need to check against 0 as pumps can't deliver negative basal anyway?
+    if (basal < 1)
+    {
+        rounded_basal = Math.round(basal * lowest_rate_scale) / lowest_rate_scale;
+    }
+    else if (basal < 10)
+    {
+        rounded_basal = Math.round(basal * 20) / 20;
+    }
+    else
+    {
+        rounded_basal = Math.round(basal * 10) / 10;
+    }
+
+    return rounded_basal;
+}
+
+exports = module.exports = round_basal

+ 48 - 0
oref0/lib/lib/temps.js

@@ -0,0 +1,48 @@
+
+function filter (treatments) {
+
+  var results = [ ];
+
+  var state = { };
+  
+  function temp (ev) {
+    if ('duration (min)' in ev) {
+      state.duration = ev['duration (min)'].toString( );
+      state.raw_duration = ev;
+    }
+
+    if ('rate' in ev) {
+      state[ev.temp] = ev.rate.toString( );
+      state.rate = ev['rate'];
+      state.raw_rate = ev;
+    }
+
+    if ('timestamp' in state && ev.timestamp !== state.timestamp) {
+      state.invalid = true;
+    } else {
+      state.timestamp = ev.timestamp;
+    }
+
+    if ('duration' in state && ('percent' in state || 'absolute' in state)) {
+      state.eventType = 'Temp Basal';
+      results.push(state);
+      state = { };
+    }
+  }
+
+  function step (current) {
+    switch (current._type) {
+      case 'TempBasalDuration':
+      case 'TempBasal':
+        temp(current);
+        break;
+      default:
+        results.push(current);
+        break;
+    }
+  }
+  treatments.forEach(step);
+  return results;
+}
+
+exports = module.exports = filter;

+ 56 - 0
oref0/lib/lib/with-raw-glucose.js

@@ -0,0 +1,56 @@
+'use strict';
+
+function cleanCal (cal) {
+  var clean = {
+    scale: parseFloat(cal.scale) || 0
+    , intercept: parseFloat(cal.intercept) || 0
+    , slope: parseFloat(cal.slope) || 0
+  };
+
+  clean.valid = ! (clean.slope === 0 || clean.unfiltered === 0 || clean.scale === 0);
+
+  return clean;
+}
+
+module.exports = function withRawGlucose (entry, cals, maxRaw) {
+  maxRaw = maxRaw || 200;
+
+  if ( entry.type === "mbg" || entry.type === "cal" ) {
+    return entry;
+  }
+  var egv = entry.glucose || entry.sgv || 0;
+
+  entry.unfiltered = parseInt(entry.unfiltered) || 0;
+  entry.filtered = parseInt(entry.filtered) || 0;
+
+  //TODO: add time check, but how recent should it be?
+  //TODO: currently assuming the first is the best (and that there is probably just 1 cal)
+  var cal = cals && cals.length > 0 && cleanCal(cals[0]);
+
+  if (cal && cal.valid) {
+    if (cal.filtered === 0 || egv < 40) {
+      entry.raw = Math.round(cal.scale * (entry.unfiltered - cal.intercept) / cal.slope);
+    } else {
+      var ratio = cal.scale * (entry.filtered - cal.intercept) / cal.slope / egv;
+      entry.raw = Math.round(cal.scale * (entry.unfiltered - cal.intercept) / cal.slope / ratio);
+    }
+
+    if ( egv < 40 ) {
+        if (entry.raw) {
+            entry.glucose = entry.raw;
+            entry.fromRaw = true;
+            if (entry.raw <= maxRaw) {
+                entry.noise = 2;
+            } else {
+                entry.noise = 3;
+            }
+        } else {
+            entry.noise = 3;
+        }
+    } else if (! entry.noise) {
+        entry.noise = 0;
+    }
+
+  }
+  return entry;
+};

+ 1 - 0
oref0_source_version.txt

@@ -0,0 +1 @@
+oref0 branch: dev - git version: 97b012a