diff --git a/bin/oref0-autotune-prep.js b/bin/oref0-autotune-prep.js index 76cda757f..5b79c525a 100755 --- a/bin/oref0-autotune-prep.js +++ b/bin/oref0-autotune-prep.js @@ -22,7 +22,7 @@ var generate = require('oref0/lib/autotune-prep'); function usage ( ) { - console.error('usage: ', process.argv.slice(0, 2), ' [carbhistory.json] [autotune/glucose.json]'); + console.error('usage: ', process.argv.slice(0, 2), ' [pumpprofile.json] [carbhistory.json] [autotune/glucose.json]'); } if (!module.parent) { @@ -33,8 +33,9 @@ if (!module.parent) { } var profile_input = process.argv.slice(3, 4).pop(); var glucose_input = process.argv.slice(4, 5).pop(); - var carb_input = process.argv.slice(5, 6).pop() - var prepped_glucose_input = process.argv.slice(6, 7).pop() + var pumpprofile_input = process.argv.slice(5, 6).pop() + var carb_input = process.argv.slice(6, 7).pop() + var prepped_glucose_input = process.argv.slice(7, 8).pop() if ( !pumphistory_input || !profile_input || !glucose_input ) { usage( ); @@ -51,10 +52,23 @@ if (!module.parent) { return console.error("Could not parse input data: ", e); } + var pumpprofile_data = { }; + if (typeof pumpprofile_input != 'undefined') { + try { + pumpprofile_data = JSON.parse(fs.readFileSync(pumpprofile_input, 'utf8')); + } catch (e) { + console.error("Warning: could not parse "+pumpprofile_input); + } + } + // disallow impossibly low carbRatios due to bad decoding if ( typeof(profile_data.carb_ratio) == 'undefined' || profile_data.carb_ratio < 2 ) { - console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratio ' + profile_data.carb_ratio + ' out of bounds" }'); - return console.error("Error: carb_ratio " + profile_data.carb_ratio + " out of bounds"); + if ( typeof(pumpprofile_data.carb_ratio) == 'undefined' || pumpprofile_data.carb_ratio < 2 ) { + console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratios ' + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + ' out of bounds" }'); + return console.error("Error: carb_ratios " + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + " out of bounds"); + } else { + profile_data.carb_ratio = pumpprofile_data.carb_ratio; + } } try { diff --git a/bin/oref0-autotune-recommends-report.sh b/bin/oref0-autotune-recommends-report.sh index 09627cc43..2c0fb89c2 100755 --- a/bin/oref0-autotune-recommends-report.sh +++ b/bin/oref0-autotune-recommends-report.sh @@ -64,7 +64,7 @@ csf_new=$(cat $directory/autotune/profile.json | jq '.csf') carb_ratio_new=$(cat $directory/autotune/profile.json | jq '.carb_ratio') # Print Header Info -printf "%-${parameter_width}s| %-${data_width}s| %-${data_width}s\n" "Parameter" "Current" "Autotune" >> $report_file +printf "%-${parameter_width}s| %-${data_width}s| %-${data_width}s\n" "Parameter" "Pump" "Autotune" >> $report_file printf "%s\n" "-------------------------------------" >> $report_file # Print ISF, CSF and Carb Ratio Recommendations @@ -74,10 +74,10 @@ if [ $csf_current != null ]; then else printf "%-${parameter_width}s| %-${data_width}s| %-${data_width}.3f\n" "CSF [mg/dL/g]" "n/a" $csf_new >> $report_file fi -printf "%-${parameter_width}s| %-${data_width}.3f| %-${data_width}.3f\n" "Carb Ratio [g]" $carb_ratio_current $carb_ratio_new >> $report_file +printf "%-${parameter_width}s| %-${data_width}.3f| %-${data_width}.3f\n" "Carb Ratio[g/U]" $carb_ratio_current $carb_ratio_new >> $report_file # Print Basal Profile Recommendations -printf "%-${parameter_width}s| %-${data_width}s|\n" "Basal Profile [unit/hour]" "-" >> $report_file +printf "%-${parameter_width}s| %-${data_width}s|\n" "Basals [U/hr]" "-" >> $report_file # Build time_list array of H:M in 30 minute increments to mirror pump basal schedule time_list=() diff --git a/bin/oref0-autotune.sh b/bin/oref0-autotune.sh index 57c911ecd..1b1ecac32 100755 --- a/bin/oref0-autotune.sh +++ b/bin/oref0-autotune.sh @@ -1,7 +1,7 @@ #!/bin/bash # This script sets up an easy test environment for autotune, allowing the user to vary parameters -# like start/end date and number of runs. +# like start/end date. # # Required Inputs: # DIR, (--dir=) @@ -10,8 +10,6 @@ # Optional Inputs: # END_DATE, (--end-date=) # if no end date supplied, assume we want a months worth or until day before current day -# NUMBER_OF_RUNS (--runs=) -# if no number of runs designated, then default to 5 # EXPORT_EXCEL (--xlsx=) # export to excel. Disabled by default # TERMINAL_LOGGING (--log @@ -39,7 +37,8 @@ DIR="" NIGHTSCOUT_HOST="" START_DATE="" END_DATE="" -NUMBER_OF_RUNS=1 # Default to a single run if not otherwise specified +START_DAYS_AGO=1 # Default to yesterday if not otherwise specified +END_DAYS_AGO=1 # Default to yesterday if not otherwise specified EXPORT_EXCEL="" # Default is to not export to Microsoft Excel TERMINAL_LOGGING=true RECOMMENDS_REPORT=true @@ -96,8 +95,12 @@ case $i in END_DATE=`date --date="$END_DATE" +%Y-%m-%d` shift # past argument=value ;; - -r=*|--runs=*) - NUMBER_OF_RUNS="${i#*=}" + -t=*|--start-days-ago=*) + START_DAYS_AGO="${i#*=}" + shift # past argument=value + ;; + -d=*|--end-days-ago=*) + END_DAYS_AGO="${i#*=}" shift # past argument=value ;; -x=*|--xlsx=*) @@ -116,28 +119,31 @@ case $i in esac done +# remove any trailing / from NIGHTSCOUT_HOST +NIGHTSCOUT_HOST=$(echo $NIGHTSCOUT_HOST | sed 's/\/$//g') + if [[ -z "$DIR" || -z "$NIGHTSCOUT_HOST" ]]; then - echo "Usage: oref0-autotune <--dir=myopenaps_directory> <--ns-host=https://mynightscout.azurewebsites.net> [--start-date=YYYY-MM-DD] [--end-date=YYYY-MM-DD] [--runs=number_of_runs] [--xlsx=autotune.xlsx] [--log=(true)|false]" + echo "Usage: oref0-autotune <--dir=myopenaps_directory> <--ns-host=https://mynightscout.azurewebsites.net> [--start-days-ago=number_of_days] [--end-days-ago=number_of_days] [--start-date=YYYY-MM-DD] [--end-date=YYYY-MM-DD] [--xlsx=autotune.xlsx] [--log=(true)|false]" exit 1 fi if [[ -z "$START_DATE" ]]; then # Default start date of yesterday - START_DATE=`date --date="1 day ago" +%Y-%m-%d` + START_DATE=`date --date="$START_DAYS_AGO days ago" +%Y-%m-%d` fi if [[ -z "$END_DATE" ]]; then # Default end-date as this morning at midnight in order to not get partial day samples for now # (ISF/CSF adjustments are still single values across each day) - END_DATE=`date --date="1 day ago" +%Y-%m-%d` + END_DATE=`date --date="$END_DAYS_AGO days ago" +%Y-%m-%d` fi if [[ -z "$UNKNOWN_OPTION" ]] ; then # everything is ok - echo "Running oref0-autotune --dir=$DIR --ns-host=$NIGHTSCOUT_HOST --start-date=$START_DATE --runs=$NUMBER_OF_RUNS --end-date=$END_DATE" + echo "Running oref0-autotune --dir=$DIR --ns-host=$NIGHTSCOUT_HOST --start-date=$START_DATE --end-date=$END_DATE" else echo "Unknown options. Exiting" exit 1 fi -# Get profile for testing copied to home directory. "openaps" is my loop directory name. +# Get profile for testing copied to home directory. cd $directory && mkdir -p autotune cp settings/pumpprofile.json autotune/profile.pump.json || die "Cannot copy settings/pumpprofile.json" # This allows manual users to be able to run autotune by simply creating a settings/pumpprofile.json file. @@ -157,15 +163,6 @@ if [[ $TERMINAL_LOGGING = "true" ]]; then exec &> >(tee -a autotune.$(date +%Y-%m-%d-%H%M%S).log) fi -# Pull Nightscout Data -echo "Grabbing NIGHTSCOUT treatments.json for date range..." - -# Get Nightscout carb and insulin Treatments -query="find%5Bcreated_at%5D%5B%24gte%5D=`date --date="$START_DATE -4 hours" -Iminutes`&find%5Bcreated_at%5D%5B%24lte%5D=`date --date="$END_DATE +1 days" -Iminutes`" -echo Query: $NIGHTSCOUT_HOST/$query -ns-get host $NIGHTSCOUT_HOST treatments.json $query > ns-treatments.json || die "Couldn't download ns-treatments.json" -ls -la ns-treatments.json || die "No ns-treatments.json downloaded" - # Build date list for autotune iteration date_list=() date=$START_DATE; @@ -179,76 +176,74 @@ do fi done -echo "Grabbing NIGHTSCOUT entries/sgv.json for date range..." +echo "Grabbing NIGHTSCOUT treatments.json and entries/sgv.json for date range..." # Get Nightscout BG (sgv.json) Entries for i in "${date_list[@]}" do - query="find%5Bdate%5D%5B%24gte%5D=`(date -d $i +%s | tr -d '\n'; echo 000)`&find%5Bdate%5D%5B%24lte%5D=`(date --date="$i +1 days" +%s | tr -d '\n'; echo 000)`&count=1000" - echo Query: $NIGHTSCOUT_HOST $query - ns-get host $NIGHTSCOUT_HOST entries/sgv.json $query > ns-entries.$i.json || die "Couldn't download ns-entries.$i.json" - ls -la ns-entries.$i.json || die "No ns-entries.$i.json downloaded" -done - -echo "Running $NUMBER_OF_RUNS runs from $START_DATE to $END_DATE" -sleep 2 - -# Do iterative runs over date range, save autotune.json (prepped data) and input/output -# profile.json -# Loop 1: Run 1 to Number of Runs specified by user or by default (1) -for run_number in $(seq 1 $NUMBER_OF_RUNS) -do - # Loop 2: Iterate through Date Range - for i in "${date_list[@]}" - do - cp profile.json profile.$run_number.$i.json + query="find%5Bdate%5D%5B%24gte%5D=`(date -d $i +%s | tr -d '\n'; echo 000)`&find%5Bdate%5D%5B%24lte%5D=`(date --date="$i +1 days" +%s | tr -d '\n'; echo 000)`&count=1000" + echo Query: $NIGHTSCOUT_HOST $query + ns-get host $NIGHTSCOUT_HOST entries/sgv.json $query > ns-entries.$i.json || die "Couldn't download ns-entries.$i.json" + ls -la ns-entries.$i.json || die "No ns-entries.$i.json downloaded" + + # Get Nightscout carb and insulin Treatments + # echo $i $START_DATE; + #query="find%5Bdate%5D%5B%24gte%5D=`(date -d $i +%s | tr -d'\n'; echo 000)`&find%5Bdate%5D%5B%24lte%5D=`(date --date="$i +1 days" +%s | tr -d '\n'; echo 000)`&count=1000" + query="find%5Bcreated_at%5D%5B%24gte%5D=`date --date="$i -5 hours" -Iminutes`&find%5Bcreated_at%5D%5B%24lte%5D=`date --date="$i +1 days" -Iminutes`" + echo Query: $NIGHTSCOUT_HOST/$query + ns-get host $NIGHTSCOUT_HOST treatments.json $query > ns-treatments.$i.json || die "Couldn't download ns-treatments.$i.json" + ls -la ns-treatments.$i.json || die "No ns-treatments.$i.json downloaded" + + + # Do iterative runs over date range, save autotune.json (prepped data) and input/output profile.json + cp profile.json profile.$i.json # Autotune Prep (required args, ), output prepped glucose # data or below - echo "oref0-autotune-prep ns-treatments.json profile.json ns-entries.$i.json > autotune.$run_number.$i.json" - oref0-autotune-prep ns-treatments.json profile.json ns-entries.$i.json > autotune.$run_number.$i.json \ - || die "Could not run oref0-autotune-prep ns-treatments.json profile.json ns-entries.$i.json" + echo "oref0-autotune-prep ns-treatments.$i.json profile.json ns-entries.$i.json profile.pump.json > autotune.$i.json" + oref0-autotune-prep ns-treatments.$i.json profile.json ns-entries.$i.json profile.pump.json > autotune.$i.json \ + || die "Could not run oref0-autotune-prep ns-treatments.$i.json profile.json ns-entries.$i.json" # Autotune (required args, ), # output autotuned profile or what will be used as in the next iteration - echo "oref0-autotune-core autotune.$run_number.$i.json profile.json profile.pump.json > newprofile.$run_number.$i.json" - if ! oref0-autotune-core autotune.$run_number.$i.json profile.json profile.pump.json > newprofile.$run_number.$i.json; then + echo "oref0-autotune-core autotune.$i.json profile.json profile.pump.json > newprofile.$i.json" + if ! oref0-autotune-core autotune.$i.json profile.json profile.pump.json > newprofile.$i.json; then if cat profile.json | jq --exit-status .carb_ratio==null; then echo "ERROR: profile.json contains null carb_ratio: using profile.pump.json" cp profile.pump.json profile.json exit else - die "Could not run oref0-autotune-core autotune.$run_number.$i.json profile.json profile.pump.json" + die "Could not run oref0-autotune-core autotune.$i.json profile.json profile.pump.json" fi else # Copy tuned profile produced by autotune to profile.json for use with next day of data - cp newprofile.$run_number.$i.json profile.json + cp newprofile.$i.json profile.json fi - done # End Date Range Iteration -done # End Number of Runs Loop -if ! [[ -z "$EXPORT_EXCEL" ]]; then - echo Exporting to $EXPORT_EXCEL - oref0-autotune-export-to-xlsx --dir $DIR --output $EXPORT_EXCEL -fi + if ! [[ -z "$EXPORT_EXCEL" ]]; then + echo Exporting to $EXPORT_EXCEL + oref0-autotune-export-to-xlsx --dir $DIR --output $EXPORT_EXCEL + fi -# Create Summary Report of Autotune Recommendations and display in the terminal -if [[ $RECOMMENDS_REPORT == "true" ]]; then - # Set the report file name, so we can let the user know where it is and cat - # it to the screen - report_file=$directory/autotune/autotune_recommendations.log + # Create Summary Report of Autotune Recommendations and display in the terminal + if [[ $RECOMMENDS_REPORT == "true" ]]; then + # Set the report file name, so we can let the user know where it is and cat + # it to the screen + report_file=$directory/autotune/autotune_recommendations.log - echo - echo "Autotune pump profile recommendations:" - echo "---------------------------------------------------------" + echo + echo "Autotune pump profile recommendations:" + echo "---------------------------------------------------------" - # Let the user know where the Autotune Recommendations are logged - echo "Recommendations Log File: $report_file" - echo + # Let the user know where the Autotune Recommendations are logged + echo "Recommendations Log File: $report_file" + echo - # Run the Autotune Recommends Report - oref0-autotune-recommends-report $directory + # Run the Autotune Recommends Report + oref0-autotune-recommends-report $directory - # Go ahead and echo autotune_recommendations.log to the terminal, minus blank lines - cat $report_file | egrep -v "\| *\| *$" -fi + # Go ahead and echo autotune_recommendations.log to the terminal, minus blank lines + cat $report_file | egrep -v "\| *\| *$" + fi + +done # End Date Range Iteration diff --git a/bin/oref0-detect-sensitivity.js b/bin/oref0-detect-sensitivity.js index 41520d594..7d3a511d9 100755 --- a/bin/oref0-detect-sensitivity.js +++ b/bin/oref0-detect-sensitivity.js @@ -17,7 +17,7 @@ var basal = require('oref0/lib/profile/basal'); var get_iob = require('oref0/lib/iob'); -var detect = require('oref0/lib/determine-basal/cob-autosens'); +var detect = require('oref0/lib/determine-basal/autosens'); if (!module.parent) { var detectsensitivity = init(); @@ -27,9 +27,10 @@ if (!module.parent) { var isf_input = process.argv.slice(4, 5).pop() var basalprofile_input = process.argv.slice(5, 6).pop() var profile_input = process.argv.slice(6, 7).pop(); + var carb_input = process.argv.slice(7, 8).pop() if (!glucose_input || !pumphistory_input || !profile_input) { - console.error('usage: ', process.argv.slice(0, 2), ' '); + console.error('usage: ', process.argv.slice(0, 2), ' [carbhistory.json]'); process.exit(1); } @@ -62,6 +63,15 @@ if (!module.parent) { } var basalprofile = require(cwd + '/' + basalprofile_input); + var carb_data = { }; + if (typeof carb_input != 'undefined') { + try { + carb_data = JSON.parse(fs.readFileSync(carb_input, 'utf8')); + } catch (e) { + console.error("Warning: could not parse "+carb_input); + } + } + var iob_inputs = { history: pumphistory_data , profile: profile @@ -73,9 +83,10 @@ if (!module.parent) { var detection_inputs = { iob_inputs: iob_inputs - , glucose_data: glucose_data - , basalprofile: basalprofile - //, clock: clock_data + , carbs: carb_data + , glucose_data: glucose_data + , basalprofile: basalprofile + //, clock: clock_data }; detect(detection_inputs); var sensAdj = { diff --git a/bin/oref0-meal.js b/bin/oref0-meal.js index b54d68306..933e04f66 100755 --- a/bin/oref0-meal.js +++ b/bin/oref0-meal.js @@ -99,7 +99,7 @@ if (!module.parent) { , glucose: glucose_data }; - var dia_carbs = generate(inputs); - console.log(JSON.stringify(dia_carbs)); + var recentCarbs = generate(inputs); + console.log(JSON.stringify(recentCarbs)); } diff --git a/bin/oref0-pump-loop.sh b/bin/oref0-pump-loop.sh index d2a580dac..0628426b8 100755 --- a/bin/oref0-pump-loop.sh +++ b/bin/oref0-pump-loop.sh @@ -12,7 +12,7 @@ main() { && refresh_old_pumphistory_24h \ && refresh_old_profile \ && touch monitor/pump_loop_enacted -r monitor/glucose.json \ - && refresh_temp_and_enact \ + && ( refresh_temp_and_enact || ( smb_verify_status && refresh_temp_and_enact ) ) \ && refresh_pumphistory_and_enact \ && refresh_profile \ && refresh_pumphistory_24h \ @@ -39,7 +39,7 @@ smb_main() { echo && echo Starting supermicrobolus pump-loop at $(date) with $upto30s second wait_for_silence: \ && wait_for_bg \ && wait_for_silence $upto30s \ - && preflight \ + && ( preflight || preflight ) \ && if_mdt_get_bg \ && refresh_old_pumphistory_24h \ && refresh_old_pumphistory \ @@ -62,8 +62,7 @@ smb_main() { )) fi ) \ - && refresh_profile \ - && refresh_pumphistory_24h \ + && ( refresh_profile; refresh_pumphistory_24h; true ) \ && echo Completed supermicrobolus pump-loop at $(date): \ && touch monitor/pump_loop_completed -r monitor/pump_loop_enacted \ && echo \ @@ -71,7 +70,7 @@ smb_main() { echo -n "SMB pump-loop failed. " if grep -q "percent" monitor/temp_basal.json; then echo "Pssst! Your pump is set to % basal type. The pump won’t accept temporary basal rates in this mode. Change it to absolute u/hr, and temporary basal rates will then be able to be set." - fi + fi maybe_mmtune echo Unsuccessful supermicrobolus pump-loop at $(date) fi @@ -316,10 +315,12 @@ function mdt_get_bg { } # make sure we can talk to the pump and get a valid model number function preflight { + echo -n "Preflight " # only 515, 522, 523, 715, 722, 723, 554, and 754 pump models have been tested with SMB ( openaps report invoke settings/model.json || openaps report invoke settings/model.json ) 2>&1 >/dev/null | tail -1 \ && egrep -q "[57](15|22|23|54)" settings/model.json \ - && echo -n "Preflight OK. " + && echo -n "OK. " \ + || ( echo -n "fail. "; false ) } # reset radio, init world wide pump (if applicable), mmtune, and wait_for_silence 60 if no signal @@ -329,10 +330,10 @@ function mmtune { reset_spi_serial.py 2>/dev/null fi oref0_init_pump_comms.py - echo -n "Listening for 30s silence before mmtuning: " + echo -n "Listening for 40s silence before mmtuning: " for i in $(seq 1 800); do echo -n . - mmeowlink-any-pump-comms.py --port $port --wait-for 30 2>/dev/null | egrep -v subg | egrep No \ + any_pump_comms 40 2>/dev/null | egrep -v subg | egrep No \ && break done echo {} > monitor/mmtune.json @@ -344,6 +345,7 @@ function mmtune { if [[ $rssi_wait > 1 ]]; then echo "waiting for $rssi_wait second silence before continuing" wait_for_silence $rssi_wait + echo "Done waiting for rigs with better signal." fi } @@ -351,28 +353,39 @@ function maybe_mmtune { if ( find monitor/ -mmin -15 | egrep -q "pump_loop_completed" ); then # mmtune ~ 25% of the time [[ $(( ( RANDOM % 100 ) )) > 75 ]] \ - && echo "Waiting for 30s silence before mmtuning" \ - && wait_for_silence 30 \ + && echo "Waiting for 40s silence before mmtuning" \ + && wait_for_silence 40 \ && mmtune else - echo "pump_loop_completed more than 15m old; waiting for 30s silence before mmtuning" - wait_for_silence 30 + echo "pump_loop_completed more than 15m old; waiting for 40s silence before mmtuning" + wait_for_silence 40 mmtune fi } +function any_pump_comms { + mmeowlink-any-pump-comms.py --port $port --wait-for $1 +} + # listen for $1 seconds of silence (no other rigs talking to pump) before continuing function wait_for_silence { if [ -z $1 ]; then - waitfor=30 + waitfor=40 else waitfor=$1 fi - ((mmeowlink-any-pump-comms.py --port $port --wait-for 1 | grep -q comms) 2>&1 | tail -1 && echo -n "Radio ok. " || mmtune) \ - && echo -n "Listening: " + # check radio multiple times, and mmtune if all checks fail + ( ( out=$(any_pump_comms 1) ; echo $out | grep -qi comms || (echo $out; false) ) || \ + ( echo -n .; sleep 1; out=$(any_pump_comms 1) ; echo $out | grep -qi comms || (echo $out; false) ) || \ + ( echo -n .; sleep 2; out=$(any_pump_comms 1) ; echo $out | grep -qi comms || (echo $out; false) ) || \ + ( echo -n .; sleep 4; out=$(any_pump_comms 1) ; echo $out | grep -qi comms || (echo $out; false) ) || \ + ( echo -n .; sleep 8; out=$(any_pump_comms 1) ; echo $out | grep -qi comms || (echo $out; false) ) + ) 2>&1 | tail -2 \ + && echo -n "Radio ok. " || (echo -n "Radio check failed. "; any_pump_comms 1 2>&1 | tail -1; mmtune) + echo -n "Listening: " for i in $(seq 1 800); do echo -n . - mmeowlink-any-pump-comms.py --port $port --wait-for $waitfor 2>/dev/null | egrep -v subg | egrep No \ + any_pump_comms $waitfor 2>/dev/null | egrep -v subg | egrep No \ && break done } @@ -439,14 +452,13 @@ function refresh_old_profile { function refresh_smb_temp_and_enact { # set mtime of monitor/glucose.json to the time of its most recent glucose value setglucosetimestamp - if ( find monitor/ -newer monitor/pump_loop_completed | grep -q glucose.json ); then - echo "glucose.json newer than pump_loop_completed. " - smb_enact_temp - elif ( find monitor/ -mmin -5 -size +5c | grep -q monitor/pump_loop_completed ); then - echo "pump_loop_completed more than 5m ago. " + # only smb_enact_temp if we haven't successfully completed a pump_loop recently + # (no point in enacting a temp that's going to get changed after we see our last SMB) + if ( find monitor/ -mmin +10 | grep -q monitor/pump_loop_completed ); then + echo "pump_loop_completed more than 10m ago: setting temp before refreshing pumphistory. " smb_enact_temp else - echo -n "pump_loop_completed less than 5m ago. " + echo -n "pump_loop_completed less than 10m ago. " fi } @@ -482,19 +494,6 @@ function refresh_profile { || (echo -n Settings refresh && openaps get-settings 2>/dev/null >/dev/null && echo ed) } -function low_battery_wait { - if (! ls monitor/edison-battery.json 2>/dev/null >/dev/null); then - echo Edison battery level not found - elif (jq --exit-status ".battery >= 98 or (.battery <= 65 and .battery >= 60)" monitor/edison-battery.json > /dev/null); then - echo "Edison battery at $(jq .battery monitor/edison-battery.json)% is charged (>= 98%) or likely charging (60-65%)" - elif (jq --exit-status ".battery < 98" monitor/edison-battery.json > /dev/null); then - echo -n "Edison on battery: $(jq .battery monitor/edison-battery.json)%; " - wait_for_bg - else - echo Edison battery level unknown - fi -} - function wait_for_bg { if grep "MDT cgm" openaps.ini 2>&1 >/dev/null; then echo "MDT CGM configured; not waiting" @@ -518,16 +517,16 @@ function wait_for_bg { function refresh_pumphistory_24h { if (! ls monitor/edison-battery.json 2>/dev/null >/dev/null); then echo -n "Edison battery level not found. " - autosens_freq=20 - elif (jq --exit-status ".battery >= 98 or (.battery <= 65 and .battery >= 60)" monitor/edison-battery.json > /dev/null); then - echo -n "Edison battery at $(jq .battery monitor/edison-battery.json)% is charged (>= 98%) or likely charging (60-65%). " - autosens_freq=20 + autosens_freq=15 + elif (jq --exit-status ".battery >= 98 or (.battery <= 70 and .battery >= 60)" monitor/edison-battery.json > /dev/null); then + echo -n "Edison battery at $(jq .battery monitor/edison-battery.json)% is charged (>= 98%) or likely charging (60-70%). " + autosens_freq=15 elif (jq --exit-status ".battery < 98" monitor/edison-battery.json > /dev/null); then echo -n "Edison on battery: $(jq .battery monitor/edison-battery.json)%. " - autosens_freq=90 + autosens_freq=30 else echo -n "Edison battery level unknown. " - autosens_freq=20 + autosens_freq=15 fi find settings/ -mmin -$autosens_freq -size +100c | grep -q pumphistory-24h-zoned && echo "Pumphistory-24 < ${autosens_freq}m old" \ || (echo -n pumphistory-24h refresh \ diff --git a/bin/oref0-setup.sh b/bin/oref0-setup.sh index 99d57833b..b69815161 100755 --- a/bin/oref0-setup.sh +++ b/bin/oref0-setup.sh @@ -138,16 +138,6 @@ if ! [[ ${CGM,,} =~ "g4-upload" || ${CGM,,} =~ "g5" || ${CGM,,} =~ "mdt" || ${CG echo DIR="" # to force a Usage prompt fi -if ! ( git config -l | grep -q user.email ) ; then - read -p "What email address would you like to use for git commits? " -r - EMAIL=$REPLY - git config --global user.email $EMAIL -fi -if ! ( git config -l | grep -q user.name ); then - read -p "What full name would you like to use for git commits? " -r - NAME=$REPLY - git config --global user.name $NAME -fi if [[ -z "$DIR" || -z "$serial" ]]; then echo "Usage: oref0-setup.sh <--dir=directory> <--serial=pump_serial_#> [--tty=/dev/ttySOMETHING] [--max_iob=0] [--ns-host=https://mynightscout.herokuapp.com] [--api-secret=[myplaintextapisecret|token=subjectname-plaintexthashsecret] [--cgm=(G4-upload|G4-local-only|shareble|G5|MDT|xdrip)] [--bleserial=SM123456] [--blemac=FE:DC:BA:98:76:54] [--btmac=AB:CD:EF:01:23:45] [--enable='autosens meal dexusb'] [--radio_locale=(WW|US)] [--ww_ti_usb_reset=(yes|no)]" echo @@ -495,11 +485,17 @@ echocolor-n "Continue? y/[N] " read -r if [[ $REPLY =~ ^[Yy]$ ]]; then + # TODO: delete this after openaps 0.2.1 release + echo Checking openaps 0.2.1 installation with --nogit support + if ! openaps --version 2>&1 | egrep "0.[2-9].[1-9]"; then + echo Installing latest openaps w/ nogit && sudo pip install git+https://github.com/openaps/openaps.git@nogit || die "Couldn't install openaps w/ nogit" + fi + echo -n "Checking $directory: " mkdir -p $directory - if ( cd $directory && git status 2>/dev/null >/dev/null && openaps use -h >/dev/null ); then + if ( cd $directory && ls openaps.ini 2>/dev/null >/dev/null && openaps use -h >/dev/null ); then echo $directory already exists - elif openaps init $directory; then + elif openaps init $directory --nogit; then echo $directory initialized else die "Can't init $directory" @@ -529,6 +525,19 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then fi mkdir -p $HOME/src/ + + # TODO: remove this and switch back to easy_install or pip once decocare 0.1.0 is released + if [ -d "$HOME/src/decocare/" ]; then + echo "$HOME/src/decocare/ already exists; pulling latest 0.1.0-dev" + (cd $HOME/src/decocare && git fetch && git checkout 0.1.0-dev && git pull) || die "Couldn't pull latest decocare 0.1.0-dev" + else + echo -n "Cloning decocare 0.1.0-dev: " + (cd $HOME/src && git clone -b 0.1.0-dev git://github.com/openaps/decocare.git) || die "Couldn't clone decocare 0.1.0-dev" + fi + echo Installing decocare 0.1.0-dev + cd $HOME/src/decocare + sudo python setup.py develop || die "Couldn't install decocare 0.1.0-dev" + if [ -d "$HOME/src/oref0/" ]; then echo "$HOME/src/oref0/ already exists; pulling latest" (cd $HOME/src/oref0 && git fetch && git pull) || die "Couldn't pull latest oref0" @@ -539,9 +548,9 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then echo Checking oref0 installation cd $HOME/src/oref0 if git branch | grep "* master"; then - npm list -g oref0 | egrep oref0@0.5.0 || (echo Installing latest oref0 package && sudo npm install -g oref0) + npm list -g oref0 | egrep oref0@0.5.[0-1] || (echo Installing latest oref0 package && sudo npm install -g oref0) else - npm list -g oref0 | egrep oref0@0.5.[1-9] || (echo Installing latest oref0 from $HOME/src/oref0/ && cd $HOME/src/oref0/ && npm run global-install) + npm list -g oref0 | egrep oref0@0.5.[2-9] || (echo Installing latest oref0 from $HOME/src/oref0/ && cd $HOME/src/oref0/ && npm run global-install) fi echo Checking mmeowlink installation @@ -578,7 +587,6 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then fi cat preferences.json - git add preferences.json # enable log rotation sudo cp $HOME/src/oref0/logrotate.openaps /etc/logrotate.d/openaps || die "Could not cp /etc/logrotate.d/openaps" @@ -661,9 +669,9 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ ${CGM,,} =~ "shareble" || ${CGM,,} =~ "g4-upload" ]]; then mkdir -p $directory-cgm-loop - if ( cd $directory-cgm-loop && git status 2>/dev/null >/dev/null && openaps use -h >/dev/null ); then + if ( cd $directory-cgm-loop && ls openaps.ini 2>/dev/null >/dev/null && openaps use -h >/dev/null ); then echo $directory-cgm-loop already exists - elif openaps init $directory-cgm-loop; then + elif openaps init $directory-cgm-loop --nogit; then echo $directory-cgm-loop initialized else die "Can't init $directory-cgm-loop" @@ -709,8 +717,6 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then cd $directory || die "Can't cd $directory" fi - grep -q pump.ini .gitignore 2>/dev/null || echo pump.ini >> .gitignore - git add .gitignore if [[ "$ttyport" =~ "spi" ]]; then echo Checking kernel for spi_serial installation @@ -968,15 +974,7 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then (crontab -l; crontab -l | grep -q "killall -g --older-than 30m oref0" || echo '* * * * * ( killall -g --older-than 30m openaps; killall -g --older-than 30m oref0-pump-loop; killall -g --older-than 30m openaps-report )') | crontab - # kill pump-loop after 5 minutes of not writing to pump-loop.log (crontab -l; crontab -l | grep -q "killall -g --older-than 5m oref0" || echo '* * * * * find /var/log/openaps/pump-loop.log -mmin +5 | grep pump && ( killall -g --older-than 5m openaps; killall -g --older-than 5m oref0-pump-loop; killall -g --older-than 5m openaps-report )') | crontab - - # repair or reset git repository if it's corrupted or disk is full - (crontab -l; crontab -l | grep -q "cd $directory && oref0-reset-git" || echo "* * * * * cd $directory && oref0-reset-git") | crontab - - # truncate git history to 1000 commits if it has grown past 1500 - (crontab -l; crontab -l | grep -q "oref0-truncate-git-history" || echo "* * * * * cd $directory && ps aux | grep -v grep | grep -q oref0-truncate-git-history || oref0-truncate-git-history") | crontab - if [[ ${CGM,,} =~ "shareble" || ${CGM,,} =~ "g4-upload" ]]; then - # repair or reset cgm-loop git repository if it's corrupted or disk is full - (crontab -l; crontab -l | grep -q "cd $directory-cgm-loop && oref0-reset-git" || echo "* * * * * cd $directory-cgm-loop && oref0-reset-git") | crontab - - # truncate cgm-loop git history to 1000 commits if it has grown past 1500 - (crontab -l; crontab -l | grep -q "cd $directory-cgm-loop && oref0-truncate-git-history" || echo "* * * * * cd $directory-cgm-loop && oref0-truncate-git-history") | crontab - (crontab -l; crontab -l | grep -q "cd $directory-cgm-loop && ps aux | grep -v grep | grep -q 'openaps monitor-cgm'" || echo "* * * * * cd $directory-cgm-loop && ps aux | grep -v grep | grep -q 'openaps monitor-cgm' || ( date; openaps monitor-cgm) | tee -a /var/log/openaps/cgm-loop.log; cp -up monitor/glucose-raw-merge.json $directory/cgm/glucose.json ; cp -up $directory/cgm/glucose.json $directory/monitor/glucose.json") | crontab - elif [[ ${CGM,,} =~ "xdrip" ]]; then (crontab -l; crontab -l | grep -q "cd $directory && ps aux | grep -v grep | grep -q 'monitor-xdrip'" || echo "* * * * * cd $directory && ps aux | grep -v grep | grep -q 'monitor-xdrip' || monitor-xdrip | tee -a /var/log/openaps/xdrip-loop.log") | crontab - diff --git a/bin/oref0-template.js b/bin/oref0-template.js deleted file mode 100755 index eaefd01bf..000000000 --- a/bin/oref0-template.js +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node - - - -var argv = require('yargs') - .usage("$0 [args]") - /* - .option('', { - }) - */ - .command('mint ', 'generate template for import', require('oref0/lib/templates/')) - // .choices('type', ['devices', 'reports', 'alias', '*']) - /* - .command('devices', 'generate common device loops', function (yargs) { - return yargs.option('b', { - alias: 'bar' - }); - }) - */ - .help('help') - .alias('h', 'help') - .completion( ) - .strict( ) - .argv - ; - -// console.log('after', argv); diff --git a/bin/oref0.sh b/bin/oref0.sh index 06938fc36..593f33525 100755 --- a/bin/oref0.sh +++ b/bin/oref0.sh @@ -20,9 +20,6 @@ $self Valid commands: oref0 device-helper - : create/template a device from bash commands easily oref0 alias-helper - : create/template a alias from bash commands easily - oref0 cron-5-minute-helper - - generate a cron template for commands - to run every 5 minutes: - oref0 cron-5-minute-helper openaps do-everything oref0 env - print information about environment. oref0 pebble oref0 ifttt-notify @@ -52,18 +49,6 @@ alias-helper) shift cat <&1 | logger -t openaps-loop - EOF ;; env) diff --git a/lib/autotune-prep/categorize.js b/lib/autotune-prep/categorize.js index 98fea1119..378301f9a 100644 --- a/lib/autotune-prep/categorize.js +++ b/lib/autotune-prep/categorize.js @@ -1,8 +1,9 @@ var tz = require('moment-timezone'); -//var calcMealCOB = require('oref0/lib/determine-basal/cob-autosens'); var basal = require('oref0/lib/profile/basal'); var getIOB = require('oref0/lib/iob'); var ISF = require('../profile/isf'); +var find_insulin = require('oref0/lib/iob/history'); +var dosed = require('./dosed'); // main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals. @@ -38,16 +39,11 @@ function categorizeBGDatums(opts) { profile: profileData , history: opts.pumpHistory }; - // TODO: verify this is safe to remove, and do so - var COBInputs = { - glucoseData: glucoseData - , IOBInputs: IOBInputs - , basalprofile: opts.basalprofile - }; - var mealCOB = 0; var CSFGlucoseData = []; var ISFGlucoseData = []; var basalGlucoseData = []; + var UAMGlucoseData = []; + var CRData = []; var bucketedData = []; bucketedData[0] = glucoseData[0]; @@ -103,10 +99,12 @@ function categorizeBGDatums(opts) { } } //console.error(treatments); - absorbing = 0; - uam = 0; // unannounced meal - mealCOB = 0; - mealCarbs = 0; + 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 for (var i=bucketedData.length-5; i > 0; --i) { @@ -117,6 +115,7 @@ function categorizeBGDatums(opts) { // 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. var treatment = treatments[treatments.length-1]; + var myCarbs = 0; if (treatment) { var treatmentDate = new Date(tz(treatment.timestamp)); var treatmentTime = treatmentDate.getTime(); @@ -125,6 +124,7 @@ function categorizeBGDatums(opts) { if (treatment.carbs >= 1) { mealCOB += parseFloat(treatment.carbs); mealCarbs += parseFloat(treatment.carbs); + myCarbs = treatment.carbs; } treatments.pop(); } @@ -148,12 +148,12 @@ function categorizeBGDatums(opts) { avgDelta = avgDelta.toFixed(2); glucoseDatum.avgDelta = avgDelta; - + //sens = ISF var sens = ISF.isfLookup(IOBInputs.profile.isfProfile,BGDate); IOBInputs.clock=BGDate.toISOString(); - // 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 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. currentBasal = basal.basalLookup(opts.basalprofile, BGDate); BGDate1hAgo = new Date(BGTime-1*60*60*1000); BGDate2hAgo = new Date(BGTime-2*60*60*1000); @@ -166,7 +166,7 @@ function categorizeBGDatums(opts) { //console.error(currentBasal,basal1hAgo,basal2hAgo,basal3hAgo,IOBInputs.profile.currentBasal); // basalBGI is BGI of basal insulin activity. - basalBGI = Math.round(( currentBasal * sens / 60 * 5 )*100)/100; // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m + 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]; @@ -189,14 +189,70 @@ function categorizeBGDatums(opts) { var profile = profileData; ci = Math.max(deviation, profile.min_5m_carbimpact); 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); } - // Store the COB, and use it as the starting point for the next data point. + + + // 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) { + CRInitialIOB = iob.iob; + CRInitialBG = glucoseDatum.glucose; + 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 { + CREndIOB = iob.iob; + CREndBG = glucoseDatum.glucose; + 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 ) { + 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 (deviation > 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; @@ -221,7 +277,8 @@ function categorizeBGDatums(opts) { console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption"); } - if (iob.iob > currentBasal || uam) { + // check that we have a decent amount of basal tuning data before excluding data as UAM + if ((iob.iob > currentBasal || uam) ) { if (deviation > 0) { uam = 1; } else { @@ -232,6 +289,7 @@ function categorizeBGDatums(opts) { console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption"); } type="uam"; + UAMGlucoseData.push(glucoseDatum); } else { if ( type === "uam" ) { console.error("end unannounced meal absorption"); @@ -247,12 +305,12 @@ function categorizeBGDatums(opts) { // attempting to prevent basal from being calculated as negative; should help prevent basals from going below 0 var minPossibleDeviation = -( basalBGI + Math.max(0,BGI) ); //var minPossibleDeviation = -basalBGI; - if ( deviation < minPossibleDeviation ) { - console.error("Adjusting deviation",deviation,"to",minPossibleDeviation.toFixed(2)); - deviation = minPossibleDeviation; - deviation = deviation.toFixed(2); - glucoseDatum.deviation = deviation; - } + //if ( deviation < minPossibleDeviation ) { + //console.error("Adjusting deviation",deviation,"to",minPossibleDeviation.toFixed(2)); + //deviation = minPossibleDeviation; + //deviation = deviation.toFixed(2); + //glucoseDatum.deviation = deviation; + //} type="basal"; basalGlucoseData.push(glucoseDatum); } else { @@ -273,7 +331,49 @@ function categorizeBGDatums(opts) { 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); } + var IOBInputs = { + profile: profileData + , history: opts.pumpHistory + }; + var 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 (4*basalLength < CSFLength) { + 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"); + var ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData); + CSFGlucoseData = []; + } + + 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"); + var basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData); + console.error("Adding",UAMLength,"UAM deviations to",ISFLength,"ISF ones"); + var ISFGlucoseData = ISFGlucoseData.concat(UAMGlucoseData); + //console.error(ISFGlucoseData.length, UAMLength); + } + return { + CRData: CRData, CSFGlucoseData: CSFGlucoseData, ISFGlucoseData: ISFGlucoseData, basalGlucoseData: basalGlucoseData diff --git a/lib/autotune-prep/dosed.js b/lib/autotune-prep/dosed.js new file mode 100644 index 000000000..41c207ab3 --- /dev/null +++ b/lib/autotune-prep/dosed.js @@ -0,0 +1,28 @@ +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); + + var rval = { + insulin: Math.round( insulinDosed * 1000 ) / 1000 + }; + + return rval; +} + +exports = module.exports = insulinDosed; diff --git a/lib/autotune-prep/index.js b/lib/autotune-prep/index.js index 0afa3dde9..971610468 100644 --- a/lib/autotune-prep/index.js +++ b/lib/autotune-prep/index.js @@ -3,7 +3,7 @@ var tz = require('moment-timezone'); var find_meals = require('oref0/lib/meal/history'); -var sum = require('./categorize'); +var categorize = require('./categorize'); function generate (inputs) { @@ -19,7 +19,7 @@ function generate (inputs) { , basalprofile: inputs.profile.basalprofile }; - var autotune_prep_output = sum(opts); + var autotune_prep_output = categorize(opts); return autotune_prep_output; } diff --git a/lib/autotune/index.js b/lib/autotune/index.js index 10739dbcd..b29f2e588 100644 --- a/lib/autotune/index.js +++ b/lib/autotune/index.js @@ -23,6 +23,9 @@ function tuneAllTheThings (inputs) { pumpCarbRatio = pumpProfile.carb_ratio; 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; @@ -31,12 +34,40 @@ function tuneAllTheThings (inputs) { //console.error(ISFGlucose[0]); var basalGlucose = preppedGlucose.basalGlucoseData; //console.error(basalGlucose[0]); + var CRData = preppedGlucose.CRData; + //console.error(CRData); + + // 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) { + CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG; + CRInsulinReq = CRBGChange / ISF; + CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB; + CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq; + //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRInsulinTotal); + CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000; + //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRInsulinTotal); + //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulinTotal,"CR:",CR); + if (CRDatum.CRInsulin > 0) { + CRTotalCarbs += CRDatum.CRCarbs; + CRTotalInsulin += CRDatum.CRInsulinTotal; + } + }); + CRTotalInsulin = Math.round(CRTotalInsulin*1000)/1000; + 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 - hourlyBasalProfile = []; - hourlyPumpProfile = []; + var hourlyBasalProfile = []; + var hourlyPumpProfile = []; for (var i=0; i < 24; i++) { - // aututuned basal profile + // autotuned basal profile for (var j=0; j < basalProfile.length; ++j) { if (basalProfile[j].minutes <= i * 60) { if (basalProfile[j].rate == 0) { @@ -70,6 +101,7 @@ function tuneAllTheThings (inputs) { } //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++) { @@ -98,8 +130,8 @@ function tuneAllTheThings (inputs) { offsetHour = hour + offset; if (offsetHour < 0) { offsetHour += 24; } //console.error(offsetHour); - hourlyBasalProfile[offsetHour].rate += basalNeeded / 3; - hourlyBasalProfile[offsetHour].rate=Math.round(hourlyBasalProfile[offsetHour].rate*1000)/1000 + 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 @@ -108,41 +140,171 @@ function tuneAllTheThings (inputs) { for (var offset=-3; offset < 0; offset++) { offsetHour = hour + offset; if (offsetHour < 0) { offsetHour += 24; } - threeHourBasal += hourlyBasalProfile[offsetHour].rate; + threeHourBasal += newHourlyBasalProfile[offsetHour].rate; } var adjustmentRatio = 1.0 + basalNeeded / threeHourBasal; //console.error(adjustmentRatio); for (var offset=-3; offset < 0; offset++) { offsetHour = hour + offset; if (offsetHour < 0) { offsetHour += 24; } - hourlyBasalProfile[offsetHour].rate = hourlyBasalProfile[offsetHour].rate * adjustmentRatio; - hourlyBasalProfile[offsetHour].rate=Math.round(hourlyBasalProfile[offsetHour].rate*1000)/1000 + newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio; + newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000 } } } if (pumpBasalProfile && pumpBasalProfile[0]) { for (var hour=0; hour < 24; hour++) { - //console.error(hourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2); + //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2); // cap adjustments at autosens_max and autosens_min autotuneMax = pumpProfile.autosens_max; autotuneMin = pumpProfile.autosens_min; var maxRate = hourlyPumpProfile[hour].rate * autotuneMax; var minRate = hourlyPumpProfile[hour].rate * autotuneMin; - if (hourlyBasalProfile[hour].rate > maxRate ) { + 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,")"); - hourlyBasalProfile[hour].rate = maxRate; - } else if (hourlyBasalProfile[hour].rate < minRate ) { + 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,")"); - hourlyBasalProfile[hour].rate = minRate; + newHourlyBasalProfile[hour].rate = minRate; + } + newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate*1000)/1000; + } + } + + // 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 (var 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); + } } - hourlyBasalProfile[hour].rate = Math.round(hourlyBasalProfile[hour].rate*1000)/1000; + //console.error(hour, newHourlyBasalProfile); + newHourlyBasalProfile[hour].rate = Math.round( (0.8*hourlyBasalProfile[hour].rate + 0.1*newHourlyBasalProfile[lastAdjustedHour].rate + 0.1*newHourlyBasalProfile[nextAdjustedHour].rate)*1000 )/1000; + 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(hourlyBasalProfile); - basalProfile = hourlyBasalProfile; + 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 (var 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); + 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% + 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); } + } + 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 + fullNewCR = carbRatio; + } else { + // how much change would be required to account for all of the deviations + fullNewCR = totalCR; + } + // only adjust by 10% + newCR = ( 0.8 * carbRatio ) + ( 0.2 * fullNewCR ); + // safety cap CR + if (typeof(pumpCarbRatio) !== 'undefined') { + var maxCR = pumpCarbRatio * autotuneMax; + var minCR = pumpCarbRatio * autotuneMin; + 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 = []; @@ -195,6 +357,12 @@ function tuneAllTheThings (inputs) { adjustedISF = minISF; } + // average both the directly-calculated ISF and the ISFFromCRAndCSF if we're reasonably well tuned + if (p50ratios > 0.4 && p50ratios < 0.6) { + // TODO: figure out if there's a way to do this without messing up CSF tuning + // adjustedISF = (adjustedISF + ISFFromCRAndCSF) / 2; + } + // and apply 10% of that adjustment var newISF = ( 0.9 * ISF ) + ( 0.1 * adjustedISF ); @@ -213,70 +381,15 @@ function tuneAllTheThings (inputs) { 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); - ISF = newISF; - - // 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 (var 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); - 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)); - } + if (newISF) { + ISF = newISF; } - // 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 10% - newCSF = ( 0.9 * CSF ) + ( 0.1 * 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); } - } - newCSF = Math.round( newCSF * 1000 ) / 1000; - console.error("totalMealCarbs:",totalMealCarbs,"totalDeviations:",totalDeviations,"fullNewCSF:",fullNewCSF,"newCSF:",newCSF); - // this is where CSF is set based on the outputs - CSF = newCSF; + // reconstruct updated version of previousAutotune as autotuneOutput autotuneOutput = previousAutotune; @@ -285,7 +398,7 @@ function tuneAllTheThings (inputs) { autotuneOutput.isfProfile = isfProfile; autotuneOutput.sens = ISF; autotuneOutput.csf = CSF; - carbRatio = ISF / CSF; + //carbRatio = ISF / CSF; carbRatio = Math.round( carbRatio * 1000 ) / 1000; autotuneOutput.carb_ratio = carbRatio; diff --git a/lib/determine-basal/autosens.js b/lib/determine-basal/autosens.js new file mode 100644 index 000000000..1dc1012f9 --- /dev/null +++ b/lib/determine-basal/autosens.js @@ -0,0 +1,369 @@ +var basal = require('oref0/lib/profile/basal'); +var get_iob = require('oref0/lib/iob'); +var find_insulin = require('oref0/lib/iob/history'); +var isf = require('../profile/isf'); +var find_meals = require('oref0/lib/meal/history'); +var tz = require('moment-timezone'); + +function detectSensitivity(inputs) { + + 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; + }); + iob_inputs = inputs.iob_inputs; + basalprofile = inputs.basalprofile; + profile = inputs.iob_inputs.profile; + + // use last 24h worth of data by default + var lastSiteChange = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)); + if (inputs.iob_inputs.profile.rewind_resets_autosens ) { + // 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]; + 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 { 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 { 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) { + 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(); + for (var 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); + 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 type=""; + //console.error(bucketed_data); + for (var i=3; i < bucketed_data.length; ++i) { + var 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; + if ( bg < 40 || bucketed_data[i-3].glucose < 40) { + 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.log(JSON.stringify(iob)); + + var bgi = Math.round(( -iob.activity * sens * 5 )*100)/100; + bgi = bgi.toFixed(2); + //console.error(delta); + deviation = delta-bgi; + deviation = deviation.toFixed(2); + + var glucoseDatum = bucketed_data[i]; + //console.error(glucoseDatum); + var BGDate = new Date(glucoseDatum.date); + var 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. + var treatment = meals[meals.length-1]; + if (treatment) { + var treatmentDate = new Date(tz(treatment.timestamp)); + var 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); + displayCOB = Math.round(mealCOB); + process.stderr.write(displayCOB.toString()); + } + meals.pop(); + } + } + + // calculate carb absorption for that 5m interval using the deviation. + if ( mealCOB > 0 ) { + //var profile = profileData; + ci = Math.max(deviation, profile.min_5m_carbimpact); + absorbed = ci * profile.carb_ratio / sens; + mealCOB = Math.max(0, mealCOB-absorbed); + } + // Store the COB, and use it as the starting point for the next data point. + + // 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 + //console.error(mealCOB, absorbing, mealCarbs); + if (mealCOB > 0 || absorbing || mealCarbs > 0) { + 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" ) { + process.stderr.write("g("); + //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" ) { + process.stderr.write(")"); + //CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end"; + //console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption"); + } + + currentBasal = iob_inputs.profile.current_basal; + if (iob.iob > currentBasal || uam) { + 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"); + } + 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"); + //console.error(bgTime); + } + // only keep the last 96 non-excluded data points (8h+ for any exclusions) + if (deviations.length > 96) { + 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 1h 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) { + pad = Math.round((1 - deviations.length/96) * 12); + console.error("Adding",pad,"more zero deviations"); + for (var d=0; d 0.1; i = i - 0.02) { + //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.02)) >= 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)); + console.error(Math.round(100*i)+"% of non-meal deviations <= 0 (target 45%-50%)"); + } + } + pSensitive = percentile(deviations, 0.50); + pResistant = percentile(deviations, 0.45); + + average = deviationSum / deviations.length; + + //console.error("Mean deviation: "+average.toFixed(2)); + var basalOff = 0; + + if(pSensitive < 0) { // sensitive + basalOff = pSensitive * (60/5) / profile.sens; + process.stderr.write("Excess insulin sensitivity detected: "); + } else if (pResistant > 0) { // resistant + basalOff = pResistant * (60/5) / profile.sens; + process.stderr.write("Excess insulin resistance detected: "); + } else { + console.error("Sensitivity normal."); + } + ratio = 1 + (basalOff / profile.max_daily_basal); + + // 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); + if (ratio != 1) { 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"); + var output = { + "ratio": ratio + } + return output; +} +module.exports = detectSensitivity; + +// 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 +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; +} + +// Returns the percentile of the given value in a sorted numeric array. +function percentRank(arr, v) { + if (typeof v !== 'number') throw new TypeError('v must be a number'); + for (var i = 0, l = arr.length; i < l; i++) { + if (v <= arr[i]) { + while (i < l && v === arr[i]) i++; + if (i === 0) return 0; + if (v !== arr[i-1]) { + i += (v - arr[i-1]) / (arr[i] - arr[i-1]); + } + return i / l; + } + } + return 1; +} + diff --git a/lib/determine-basal/cob-autosens.js b/lib/determine-basal/cob.js similarity index 54% rename from lib/determine-basal/cob-autosens.js rename to lib/determine-basal/cob.js index 8d05e9a65..3eacceef1 100644 --- a/lib/determine-basal/cob-autosens.js +++ b/lib/determine-basal/cob.js @@ -3,7 +3,7 @@ var get_iob = require('oref0/lib/iob'); var find_insulin = require('oref0/lib/iob/history'); var isf = require('../profile/isf'); -function detectSensitivityandCarbAbsorption(inputs) { +function detectCarbAbsorption(inputs) { glucose_data = inputs.glucose_data.map(function prepGlucose (obj) { //Support the NS sgv field to avoid having to convert in a custom way @@ -18,25 +18,6 @@ function detectSensitivityandCarbAbsorption(inputs) { //console.error(mealTime, ciTime); - // use last 24h worth of data by default - var lastSiteChange = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)); - if (inputs.iob_inputs.profile.rewind_resets_autosens && ! inputs.mealTime ) { - // 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); @@ -69,16 +50,14 @@ function detectSensitivityandCarbAbsorption(inputs) { continue; } // only consider BGs for 6h after a meal for calculating COB - if (mealTime) { - hoursAfterMeal = (bgTime-mealTime)/(60*60*1000); - if (hoursAfterMeal > 6 || foundPreMealBG) { - continue; - } else if (hoursAfterMeal < 0) { + 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); + 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 hour of data in CI mode // this allows us to calculate deviations for the last ~45m if (typeof ciTime) { @@ -87,13 +66,6 @@ function detectSensitivityandCarbAbsorption(inputs) { continue; } } - // only consider BGs since lastSiteChange - if (lastSiteChange) { - hoursSinceSiteChange = (bgTime-lastSiteChange)/(60*60*1000); - if (hoursSinceSiteChange < 0) { - continue; - } - } var elapsed_minutes = (bgTime - lastbgTime)/(60*1000); //if (foundPreMealBG) { console.error(bgTime, lastbgTime, elapsed_minutes); } if(Math.abs(elapsed_minutes) > 8) { @@ -182,24 +154,6 @@ function detectSensitivityandCarbAbsorption(inputs) { //console.error("Deviations:",bgTime, avgDeviation, deviationSlope, minDeviationSlope); } - // Exclude large positive deviations (carb absorption) from autosens - if (avgDelta-bgi < 6) { - if ( deviation > 0 ) { - inputs.mealTime || process.stderr.write("+"); - } else if ( deviation == 0 ) { - inputs.mealTime || process.stderr.write("="); - } else { - inputs.mealTime || process.stderr.write("-"); - } - avgDeltas.push(avgDelta); - bgis.push(bgi); - deviations.push(deviation); - deviationSum += parseFloat(deviation); - } else { - inputs.mealTime || process.stderr.write(">"); - //console.error(bgTime); - } - // if bgTime is more recent than mealTime if(bgTime > mealTime) { // figure out how many carbs that represents @@ -214,105 +168,13 @@ function detectSensitivityandCarbAbsorption(inputs) { if(maxDeviation>0) { //console.error("currentDeviation:",currentDeviation,"maxDeviation:",maxDeviation,"minDeviationSlope:",minDeviationSlope); } - //console.error(""); - inputs.mealTime || process.stderr.write(" "); - //console.log(JSON.stringify(avgDeltas)); - //console.log(JSON.stringify(bgis)); - // when we have less than 12h worth of deviation data, add up to 1h of zero deviations - // this dampens any large sensitivity changes detected based on too little data, without ignoring them completely - if (! inputs.mealTime && deviations.length < 144) { - pad = Math.round((1 - deviations.length/144) * 12); - console.error("Found",deviations.length,"deviations since",lastSiteChange,"- adding",pad,"more zero deviations"); - for (var d=0; d 0.1; i = i - 0.02) { - //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.02)) >= 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)); - inputs.mealTime || console.error(Math.round(100*i)+"% of non-meal deviations <= 0 (target 45%-50%)"); - } - } - pSensitive = percentile(deviations, 0.50); - pResistant = percentile(deviations, 0.45); - - average = deviationSum / deviations.length; - - //console.error("Mean deviation: "+average.toFixed(2)); - var basalOff = 0; - - if(pSensitive < 0) { // sensitive - basalOff = pSensitive * (60/5) / profile.sens; - inputs.mealTime || process.stderr.write("Excess insulin sensitivity detected: "); - } else if (pResistant > 0) { // resistant - basalOff = pResistant * (60/5) / profile.sens; - inputs.mealTime || process.stderr.write("Excess insulin resistance detected: "); - } else { - inputs.mealTime || console.error("Sensitivity normal."); - } - ratio = 1 + (basalOff / profile.max_daily_basal); - - // 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) { - inputs.mealTime || console.error('Ratio limited from ' + rawRatio + ' to ' + ratio); - } - - ratio = Math.round(ratio*100)/100; - newisf = Math.round(profile.sens / ratio); - if (ratio != 1) { inputs.mealTime || 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"); var output = { - "ratio": ratio - , "carbsAbsorbed": carbsAbsorbed + "carbsAbsorbed": carbsAbsorbed , "currentDeviation": currentDeviation , "maxDeviation": maxDeviation , "minDeviationSlope": minDeviationSlope } return output; } -module.exports = detectSensitivityandCarbAbsorption; - -// 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 -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; -} - -// Returns the percentile of the given value in a sorted numeric array. -function percentRank(arr, v) { - if (typeof v !== 'number') throw new TypeError('v must be a number'); - for (var i = 0, l = arr.length; i < l; i++) { - if (v <= arr[i]) { - while (i < l && v === arr[i]) i++; - if (i === 0) return 0; - if (v !== arr[i-1]) { - i += (v - arr[i-1]) / (arr[i] - arr[i-1]); - } - return i / l; - } - } - return 1; -} - +module.exports = detectCarbAbsorption; diff --git a/lib/determine-basal/determine-basal.js b/lib/determine-basal/determine-basal.js index ce8c2708e..a34f51d56 100644 --- a/lib/determine-basal/determine-basal.js +++ b/lib/determine-basal/determine-basal.js @@ -26,12 +26,12 @@ function round(value, digits) // we expect BG to rise or fall at the rate of BGI, // adjusted by the rate at which BG would need to rise / -// fall to get eventualBG to target over DIA/2 hours -function calculate_expected_delta(dia, target_bg, eventual_bg, bgi) { - // (hours * mins_per_hour) / 5 = how many 5 minute periods in dia/2 - var dia_in_5min_blocks = (dia/2 * 60) / 5; +// fall to get eventualBG to target over 2 hours +function calculate_expected_delta(target_bg, eventual_bg, bgi) { + // (hours * mins_per_hour) / 5 = how many 5 minute periods in 2h = 24 + var five_min_blocks = (2 * 60) / 5; var target_delta = target_bg - eventual_bg; - var expectedDelta = round(bgi + (target_delta / dia_in_5min_blocks), 1); + var expectedDelta = round(bgi + (target_delta / five_min_blocks), 1); return expectedDelta; } @@ -170,6 +170,10 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // don't overreact to a big negative delta: use minAvgDelta if deviation is negative if (deviation < 0) { deviation = round( (30 / 5) * ( minAvgDelta - bgi ) ); + // and if deviation is still negative, use long_avgdelta + if (deviation < 0) { + deviation = round( (30 / 5) * ( glucose_status.long_avgdelta - bgi ) ); + } } // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity @@ -217,7 +221,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } } - var expectedDelta = calculate_expected_delta(profile.dia, target_bg, eventualBG, bgi); + var expectedDelta = calculate_expected_delta(target_bg, eventualBG, bgi); if (typeof eventualBG === 'undefined' || isNaN(eventualBG)) { rT.error ='Error: could not calculate eventualBG. '; return rT; @@ -236,7 +240,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ , 'eventualBG': eventualBG , 'snoozeBG': snoozeBG , 'insulinReq': 0 - , 'reservoir' : reservoir_data // The expected reservoir volume at which to deliver the microbolus (the reservoir volume from immediately before the last pumphistory run) + , 'reservoir' : reservoir_data // The expected reservoir volume at which to deliver the microbolus (the reservoir volume from right before the last pumphistory run) , 'deliverAt' : deliverAt // The time at which the microbolus should be delivered , 'minPredBG' : 999 }; @@ -258,7 +262,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ UAMpredBGs.push(bg); // enable SMB whenever we have COB or UAM is enabled - // SMB is diabled by default, unless explicitly enabled in preferences.json + // SMB is disabled by default, unless explicitly enabled in preferences.json var enableSMB=false; // disable SMB when a high temptarget is set if (profile.temptargetSet && target_bg > 100) { @@ -290,18 +294,30 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ ci = round((minDelta - bgi),1); uci = round((minAvgDelta - bgi),1); // ISF (mg/dL/U) / CR (g/U) = CSF (mg/dL/g) - var csf = sens / profile.carb_ratio + // use profile.sens instead of autosens-adjusted sens to avoid counteracting + // autosens meal insulin dosing adjustmenst when sensitive/resistant + var csf = profile.sens / profile.carb_ratio // set meal_carbimpact high enough to absorb all meal carbs over 6 hours // total_impact (mg/dL) = CSF (mg/dL/g) * carbs (g) //console.error(csf * meal_data.carbs); // meal_carbimpact (mg/dL/5m) = CSF (mg/dL/g) * carbs (g) / 6 (h) * (1h/60m) * 5 (m/5m) * 2 (for linear decay) //var meal_carbimpact = round((csf * meal_data.carbs / 6 / 60 * 5 * 2),1) - // calculate the number of carbs absorbed over 4h at current CI + // assume that all carbs will absorb over 4 more hours w/ normal targets + var remainingCATime = 4; + // if an eating soon low temp target is set, assume a 3h absorption time (for 80 mg/dL) + if (profile.temptargetSet && ( target_bg < 90 || target_bg > 110 )) { + // conversely, assume 5h for 120, 6h for 140, etc. + // number of hours to adjust remainingCATime. 80 -> -1, 120 -> +1, 140 -> +2 + remainingCATimeAdjustment = (target_bg - 100) / 20; + remainingCATime += remainingCATimeAdjustment; + console.error("Adjusting remainingCATime to",remainingCATime,"h based on temp target of",target_bg); + } + + // calculate the number of carbs absorbed over remainingCATime hours at current CI // CI (mg/dL/5m) * (5m)/5 (m) * 60 (min/hr) * 4 (h) / 2 (linear decay factor) = total carb impact (mg/dL) - var totalCI = Math.max(0, ci / 5 * 60 * 4 / 2); + var totalCI = Math.max(0, ci / 5 * 60 * remainingCATime / 2); // totalCI (mg/dL) / CSF (mg/dL/g) = total carbs absorbed (g) var totalCA = totalCI / csf; - // exclude the last 1/3 of carbs from remainingCarbs, and then cap it at 90 var remainingCarbsCap = 90; // default to 90 var remainingCarbsFraction = 1; if (profile.remainingCarbsCap) { remainingCarbsCap = Math.min(90,profile.remainingCarbsCap); } @@ -309,10 +325,12 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var remainingCarbsIgnore = 1 - remainingCarbsFraction; var remainingCarbs = Math.max(0, meal_data.mealCOB - totalCA - meal_data.carbs*remainingCarbsIgnore); remainingCarbs = Math.min(remainingCarbsCap,remainingCarbs); - // assume remainingCarbs will absorb over 4h - // remainingCI (mg/dL/5m) = remainingCarbs (g) * CSF (mg/dL/g) * 5 (m/5m) * 1h/60m / 4 (h) - var remainingCI = remainingCarbs * csf * 5 / 60 / 4; - //console.error(profile.min_5m_carbimpact,ci,totalCI,totalCA,remainingCarbs,remainingCI); + // assume remainingCarbs will absorb in a /\ shaped bilinear curve + // peaking at remainingCATime / 2 and ending at remainingCATime hours + // area of the /\ triangle is the same as a remainingCIpeak-height rectangle out to remainingCATime/2 + // remainingCIpeak (mg/dL/5m) = remainingCarbs (g) * CSF (mg/dL/g) * 5 (m/5m) * 1h/60m / (remainingCATime/2) (h) + var remainingCIpeak = remainingCarbs * csf * 5 / 60 / (remainingCATime/2); + //console.error(profile.min_5m_carbimpact,ci,totalCI,totalCA,remainingCarbs,remainingCI,remainingCATime); //if (meal_data.mealCOB * 3 > meal_data.carbs) { } // calculate peak deviation in last hour, and slope from that to current deviation @@ -322,10 +340,11 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ aci = 10; //5m data points = g * (1U/10g) * (40mg/dL/1U) / (mg/dL/5m) // duration (in 5m data points) = COB (g) * CSF (mg/dL/g) / ci (mg/dL/5m) - cid = Math.max(0, meal_data.mealCOB * csf / ci ); + // limit cid to remainingCATime hours: the reset goes to remainingCI + cid = Math.min(remainingCATime*60/5/2,Math.max(0, meal_data.mealCOB * csf / ci )); acid = Math.max(0, meal_data.mealCOB * csf / aci ); // duration (hours) = duration (5m) * 5 / 60 * 2 (to account for linear decay) - console.error("Carb Impact:",ci,"mg/dL per 5m; CI Duration:",round(cid*5/60*2,1),"hours; remaining 4h+ CI:",round(remainingCI,1),"mg/dL per 5m"); + console.error("Carb Impact:",ci,"mg/dL per 5m; CI Duration:",round(cid*5/60*2,1),"hours; remaining CI (2h peak):",round(remainingCIpeak,1),"mg/dL per 5m"); console.error("Accel. Carb Impact:",aci,"mg/dL per 5m; ACI Duration:",round(acid*5/60*2,1),"hours"); var minIOBPredBG = 999; var minCOBPredBG = 999; @@ -342,6 +361,9 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var lastCOBpredBG; var lastUAMpredBG; var UAMduration = 0; + var remainingCItotal = 0; + var remainingCIs = []; + var predCIs = []; try { iobArray.forEach(function(iobTick) { //console.error(iobTick); @@ -355,37 +377,43 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // eventually accounting for all carbs (if they can be absorbed over DIA) predCI = Math.max(0, Math.max(0,ci) * ( 1 - COBpredBGs.length/Math.max(cid*2,1) ) ); predACI = Math.max(0, Math.max(0,aci) * ( 1 - COBpredBGs.length/Math.max(acid*2,1) ) ); - // if any carbs aren't absorbed after 4 hours, assume they'll absorb at a constant rate for next 4h + // if any carbs aren't absorbed after remainingCATime hours, assume they'll absorb in a /\ shaped + // bilinear curve peaking at remainingCIpeak at remainingCATime/2 hours (remainingCATime/2*12 * 5m) + // and ending at remainingCATime h (remainingCATime*12 * 5m intervals) + var intervals = Math.min( COBpredBGs.length, (remainingCATime*12)-COBpredBGs.length ); + var remainingCI = Math.max(0, intervals / (remainingCATime/2*12) * remainingCIpeak ); + remainingCItotal += predCI+remainingCI; + remainingCIs.push(round(remainingCI,1)); + predCIs.push(round(predCI,1)); + //process.stderr.write(round(predCI,1)+"+"+round(remainingCI,1)+" "); COBpredBG = COBpredBGs[COBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predCI + remainingCI; - // stop adding remainingCI after 4h - if (COBpredBGs.length > 4 * 60 / 5) { remainingCI = 0; } aCOBpredBG = aCOBpredBGs[aCOBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predACI; // for UAMpredBGs, predicted carb impact drops at minDeviationSlope // calculate predicted CI from UAM based on minDeviationSlope predUCIslope = Math.max(0, uci + ( UAMpredBGs.length*minDeviationSlope ) ); // if minDeviationSlope is too flat, predicted deviation impact drops linearly from - // current deviation down to zero over DIA (data points every 5m) - predUCIdia = Math.max(0, uci * ( 1 - UAMpredBGs.length/Math.max(profile.dia*60/5,1) ) ); - //console.error(predUCIslope, predUCIdia); + // current deviation down to zero over 3h (data points every 5m) + predUCImax = Math.max(0, uci * ( 1 - UAMpredBGs.length/Math.max(3*60/5,1) ) ); + //console.error(predUCIslope, predUCImax); // predicted CI from UAM is the lesser of CI based on deviationSlope or DIA - predUCI = Math.min(predUCIslope, predUCIdia); + predUCI = Math.min(predUCIslope, predUCImax); if(predUCI>0) { //console.error(UAMpredBGs.length,minDeviationSlope, predUCI); UAMduration=round((UAMpredBGs.length+1)*5/60,1); } UAMpredBG = UAMpredBGs[UAMpredBGs.length-1] + predBGI + Math.min(0, predDev) + predUCI; //console.error(predBGI, predCI, predUCI); - // truncate all BG predictions at 3.5 hours - if ( IOBpredBGs.length < 42) { IOBpredBGs.push(IOBpredBG); } - if ( COBpredBGs.length < 42) { COBpredBGs.push(COBpredBG); } - if ( aCOBpredBGs.length < 42) { aCOBpredBGs.push(aCOBpredBG); } - if ( UAMpredBGs.length < 42) { UAMpredBGs.push(UAMpredBG); } + // truncate all BG predictions at 4 hours + if ( IOBpredBGs.length < 48) { IOBpredBGs.push(IOBpredBG); } + if ( COBpredBGs.length < 48) { COBpredBGs.push(COBpredBG); } + if ( aCOBpredBGs.length < 48) { aCOBpredBGs.push(aCOBpredBG); } + if ( UAMpredBGs.length < 48) { UAMpredBGs.push(UAMpredBG); } // wait 90m before setting minIOBPredBG if ( IOBpredBGs.length > 18 && (IOBpredBG < minIOBPredBG) ) { minIOBPredBG = round(IOBpredBG); } if ( IOBpredBG > maxIOBPredBG ) { maxIOBPredBG = IOBpredBG; } - // wait 60m before setting COB and UAM minPredBGs - if ( (cid || remainingCI > 0) && COBpredBGs.length > 12 && (COBpredBG < minCOBPredBG) ) { minCOBPredBG = round(COBpredBG); } - if ( (cid || remainingCI > 0) && COBpredBG > maxIOBPredBG ) { maxCOBPredBG = COBpredBG; } + // wait 90m before setting COB and 60m for UAM minPredBGs + if ( (cid || remainingCIpeak > 0) && COBpredBGs.length > 18 && (COBpredBG < minCOBPredBG) ) { minCOBPredBG = round(COBpredBG); } + if ( (cid || remainingCIpeak > 0) && COBpredBG > maxIOBPredBG ) { maxCOBPredBG = COBpredBG; } if ( enableUAM && UAMpredBGs.length > 12 && (UAMpredBG < minUAMPredBG) ) { minUAMPredBG = round(UAMpredBG); } if ( enableUAM && UAMpredBG > maxIOBPredBG ) { maxUAMPredBG = UAMpredBG; } }); @@ -394,6 +422,10 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } catch (e) { console.error("Problem with iobArray. Optional feature Advanced Meal Assist disabled:",e); } + console.error("predCIs:",predCIs.join(" ")); + console.error("remainingCIs:",remainingCIs.join(" ")); + console.error("COB:",meal_data.mealCOB,"remainingCItotal/csf:",round(remainingCItotal/csf,2),"remainingCarbs:",round(remainingCarbs,2)); + //,"totalCA:",round(totalCA,2),"remainingCItotal/csf+totalCA:",round(remainingCItotal/csf+totalCA,2)); rT.predBGs = {}; IOBpredBGs.forEach(function(p, i, theArray) { theArray[i] = round(Math.min(401,Math.max(39,p))); @@ -414,7 +446,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } rT.predBGs.aCOB = aCOBpredBGs; } - if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCI > 0 )) { + if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { COBpredBGs.forEach(function(p, i, theArray) { theArray[i] = round(Math.min(401,Math.max(39,p))); }); @@ -426,7 +458,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ lastCOBpredBG=round(COBpredBGs[COBpredBGs.length-1]); eventualBG = Math.max(eventualBG, round(COBpredBGs[COBpredBGs.length-1]) ); } - if (ci > 0 || remainingCI > 0) { + if (ci > 0 || remainingCIpeak > 0) { if (enableUAM) { UAMpredBGs.forEach(function(p, i, theArray) { theArray[i] = round(Math.min(401,Math.max(39,p))); @@ -451,9 +483,11 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ minUAMPredBG = Math.max(39,minUAMPredBG); minPredBG = round(minIOBPredBG); + var fractionCarbsLeft = meal_data.mealCOB/meal_data.carbs; // if we have COB and UAM is enabled, average all three if ( minUAMPredBG < 400 && minCOBPredBG < 400 ) { - avgPredBG = round( (IOBpredBG + UAMpredBG + COBpredBG)/3 ); + // weight COBpredBG vs. UAMpredBG based on how many carbs remain as COB + avgPredBG = round( (IOBpredBG/3 + (1-fractionCarbsLeft)*UAMpredBG*2/3 + fractionCarbsLeft*COBpredBG*2/3) ); // if UAM is disabled, average IOB and COB } else if ( minCOBPredBG < 400 ) { avgPredBG = round( (IOBpredBG + COBpredBG)/2 ); @@ -482,7 +516,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // if we have COB, use minCOBPredBG, or blendedMinPredBG if it's higher } else if ( minCOBPredBG < 400 ) { // calculate blendedMinPredBG based on how many carbs remain as COB - fractionCarbsLeft = meal_data.mealCOB/meal_data.carbs; blendedMinPredBG = fractionCarbsLeft*minCOBPredBG + (1-fractionCarbsLeft)*avgMinPredBG; // if blendedMinPredBG > minCOBPredBG, use that instead minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG, blendedMinPredBG)); @@ -531,7 +564,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var bgUndershoot = target_bg - Math.max( naive_eventualBG, eventualBG, lastIOBpredBG ); // calculate how long until COB (or IOB) predBGs drop below min_bg var minutesAboveMinBG = 240; - if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCI > 0 )) { + if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { for (var i=0; i 0 ) { + if ( carbsReq >= profile.carbsReqThreshold ) { rT.carbsReq = carbsReq; rT.reason += carbsReq + " add'l carbs req + " + minutesAboveMinBG + "m zero temp; "; } @@ -615,76 +648,74 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } } - if (eventualBG < min_bg) { - // if we've bolused recently, we can snooze until the bolus IOB decays (at double speed) - if (snoozeBG > min_bg) { // if adding back in the bolus contribution BG would be above min - // If we're not in SMB mode with COB, or lastCOBpredBG > target_bg, bolus snooze - if (! (microBolusAllowed && rT.COB) || lastCOBpredBG > target_bg) { - rT.reason += ", bolus snooze: eventual BG range " + convert_bg(eventualBG, profile) + "-" + convert_bg(snoozeBG, profile); - //console.error(currenttemp, basal ); - if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { - rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. "; - return rT; - } else { - rT.reason += "; setting current basal of " + basal + " as temp. "; - return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); - } - } - } else { - // calculate 30m low-temp required to get projected BG up to target - // use snoozeBG to more gradually ramp in any counteraction of the user's boluses - // multiply by 2 to low-temp faster for increased hypo safety - var insulinReq = 2 * Math.min(0, (snoozeBG - target_bg) / sens); - insulinReq = round( insulinReq , 2); - // calculate naiveInsulinReq based on naive_eventualBG - var naiveInsulinReq = Math.min(0, (naive_eventualBG - target_bg) / sens); - naiveInsulinReq = round( naiveInsulinReq , 2); - if (minDelta < 0 && minDelta > expectedDelta) { - // if we're barely falling, newinsulinReq should be barely negative - rT.reason += ", Snooze BG " + convert_bg(snoozeBG, profile); - var newinsulinReq = round(( insulinReq * (minDelta / expectedDelta) ), 2); - //console.error("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq); - insulinReq = newinsulinReq; - } - // rate required to deliver insulinReq less insulin over 30m: - var rate = basal + (2 * insulinReq); - rate = round_basal(rate, profile); - // if required temp < existing temp basal - var insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60; - // if current temp would deliver a lot (30% of basal) less than the required insulin, - // by both normal and naive calculations, then raise the rate - var minInsulinReq = Math.min(insulinReq,naiveInsulinReq); - if (insulinScheduled < minInsulinReq - basal*0.3) { - rT.reason += ", "+currenttemp.duration + "m@" + (currenttemp.rate).toFixed(2) + " is a lot less than needed. "; - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); - } - if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate >= currenttemp.rate * 0.8)) { - rT.reason += ", temp " + currenttemp.rate + " ~< req " + rate + "U/hr. "; + // if we've bolused recently, we can snooze until the bolus IOB decays (at double speed) + if (snoozeBG > min_bg) { // if adding back in the bolus contribution BG would be above min + // If we're not in SMB mode with COB, or lastCOBpredBG > target_bg, bolus snooze + if (! (microBolusAllowed && rT.COB) || lastCOBpredBG > target_bg) { + rT.reason += ", bolus snooze: eventual BG range " + convert_bg(eventualBG, profile) + "-" + convert_bg(snoozeBG, profile); + //console.error(currenttemp, basal ); + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. "; return rT; } else { - // calculate a long enough zero temp to eventually correct back up to target - if ( rate < 0 ) { - var bgUndershoot = target_bg - naive_eventualBG; - var worstCaseInsulinReq = bgUndershoot / sens; - var durationReq = round(60*worstCaseInsulinReq / profile.current_basal); - if (durationReq < 0) { - durationReq = 0; - // don't set a temp longer than 120 minutes - } else { - durationReq = round(durationReq/30)*30; - durationReq = Math.min(120,Math.max(0,durationReq)); - } - //console.error(durationReq); - //rT.reason += "insulinReq " + insulinReq + "; " - if (durationReq > 0) { - rT.reason += ", setting " + durationReq + "m zero temp. "; - return tempBasalFunctions.setTempBasal(rate, durationReq, profile, rT, currenttemp); - } + rT.reason += "; setting current basal of " + basal + " as temp. "; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } + } else { + // calculate 30m low-temp required to get projected BG up to target + // use snoozeBG to more gradually ramp in any counteraction of the user's boluses + // multiply by 2 to low-temp faster for increased hypo safety + var insulinReq = 2 * Math.min(0, (snoozeBG - target_bg) / sens); + insulinReq = round( insulinReq , 2); + // calculate naiveInsulinReq based on naive_eventualBG + var naiveInsulinReq = Math.min(0, (naive_eventualBG - target_bg) / sens); + naiveInsulinReq = round( naiveInsulinReq , 2); + if (minDelta < 0 && minDelta > expectedDelta) { + // if we're barely falling, newinsulinReq should be barely negative + rT.reason += ", Snooze BG " + convert_bg(snoozeBG, profile); + var newinsulinReq = round(( insulinReq * (minDelta / expectedDelta) ), 2); + //console.error("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq); + insulinReq = newinsulinReq; + } + // rate required to deliver insulinReq less insulin over 30m: + var rate = basal + (2 * insulinReq); + rate = round_basal(rate, profile); + // if required temp < existing temp basal + var insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60; + // if current temp would deliver a lot (30% of basal) less than the required insulin, + // by both normal and naive calculations, then raise the rate + var minInsulinReq = Math.min(insulinReq,naiveInsulinReq); + if (insulinScheduled < minInsulinReq - basal*0.3) { + rT.reason += ", "+currenttemp.duration + "m@" + (currenttemp.rate).toFixed(2) + " is a lot less than needed. "; + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } + if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate >= currenttemp.rate * 0.8)) { + rT.reason += ", temp " + currenttemp.rate + " ~< req " + rate + "U/hr. "; + return rT; + } else { + // calculate a long enough zero temp to eventually correct back up to target + if ( rate < 0 ) { + var bgUndershoot = target_bg - naive_eventualBG; + var worstCaseInsulinReq = bgUndershoot / sens; + var durationReq = round(60*worstCaseInsulinReq / profile.current_basal); + if (durationReq < 0) { + durationReq = 0; + // don't set a temp longer than 120 minutes } else { - rT.reason += ", setting " + rate + "U/hr. "; + durationReq = round(durationReq/30)*30; + durationReq = Math.min(120,Math.max(0,durationReq)); + } + //console.error(durationReq); + //rT.reason += "insulinReq " + insulinReq + "; " + if (durationReq > 0) { + rT.reason += ", setting " + durationReq + "m zero temp. "; + return tempBasalFunctions.setTempBasal(rate, durationReq, profile, rT, currenttemp); } - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } else { + rT.reason += ", setting " + rate + "U/hr. "; } + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); } } } @@ -732,15 +763,9 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } // eventualBG, snoozeBG, or minPredBG is below max_bg if (Math.min(eventualBG,snoozeBG,minPredBG) < max_bg) { - // if there is a high-temp running and eventualBG > max_bg, let it run - if (eventualBG > max_bg && round_basal(currenttemp.rate, profile) > round_basal(basal, profile) && currenttemp.duration > 5 ) { - rT.reason += eventualBG + " > " + max_bg + ": no temp required (letting high temp of " + currenttemp.rate + " run). " - return rT; - } - // if in SMB mode, don't cancel SMB zero temp if (! (microBolusAllowed && enableSMB )) { - rT.reason += convert_bg(eventualBG, profile)+"-"+convert_bg(Math.min(minPredBG,snoozeBG), profile)+" in range: no temp required"; + rT.reason += convert_bg(eventualBG, profile)+"-"+convert_bg(Math.min(minPredBG,snoozeBG), profile)+" in range: bolus snooze, no temp required"; if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. "; return rT; @@ -801,10 +826,30 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // only allow microboluses with COB or low temp targets, or within DIA hours of a bolus // only microbolus if 0.1U SMB represents 20m or less of basal (0.3U/hr or higher) if (microBolusAllowed && enableSMB && profile.current_basal >= 0.3 && bg > threshold) { - // never bolus more than 30m worth of basal - maxBolus = round(profile.current_basal/2,1); + // never bolus more than maxSMBBasalMinutes worth of basal + mealInsulinReq = round( meal_data.mealCOB / profile.carb_ratio ,3); + if (typeof profile.maxSMBBasalMinutes == 'undefined' ) { + maxBolus = round( profile.current_basal * 30 / 60 ,1); + console.error("profile.maxSMBBasalMinutes undefined: defaulting to 30m"); + // if IOB covers more than COB, limit maxBolus to 30m of basal + } else if ( iob_data.iob > mealInsulinReq && iob_data.iob > 0 ) { + console.error("IOB",iob_data.iob,"> COB",meal_data.mealCOB+"; mealInsulinReq =",mealInsulinReq); + maxBolus = round( profile.current_basal * 30 / 60 ,1); + } else { + console.error("profile.maxSMBBasalMinutes:",profile.maxSMBBasalMinutes,"profile.current_basal:",profile.current_basal); + maxBolus = round( profile.current_basal * profile.maxSMBBasalMinutes / 60 ,1); + } // bolus 1/3 the insulinReq, up to maxBolus microBolus = round(Math.min(insulinReq/3,maxBolus),1); + // if IOB doesn't cover COB, microBolus 1/2 the insulinReq + // (or enough insulin to cover COB, or maxBolus, whichever is smallest) + if ( iob_data.iob < mealInsulinReq ) { + initialMealInsulinReq = round(mealInsulinReq-iob_data.iob,1); + console.error("IOB",iob_data.iob,"< COB",meal_data.mealCOB+"; insulinReq/2 =",insulinReq/2+"; initialMealInsulinReq =",initialMealInsulinReq); + if (initialMealInsulinReq > microBolus) { + microBolus = round(Math.min(insulinReq/2,maxBolus,initialMealInsulinReq),1); + } + } // calculate a long enough zero temp to eventually correct back up to target var smbTarget = target_bg; @@ -816,27 +861,32 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ durationReq = 0; } - if (durationReq < 0) { + var smbLowTempReq = 0; + if (durationReq <= 0) { durationReq = 0; // don't set a temp longer than 120 minutes - } else { + } else if (durationReq >= 30) { durationReq = round(durationReq/30)*30; durationReq = Math.min(120,Math.max(0,durationReq)); + } else { + // if SMB durationReq is less than 30m, set a nonzero low temp + smbLowTempReq = round( basal * durationReq/30 ,2); + durationReq = 30; } rT.reason += " insulinReq " + insulinReq; if (microBolus >= maxBolus) { rT.reason += "; maxBolus " + maxBolus; } if (durationReq > 0) { - rT.reason += "; setting " + durationReq + "m zero temp"; + rT.reason += "; setting " + durationReq + "m low temp of " + smbLowTempReq + "U/h"; } rT.reason += ". "; - //allow SMBs every 3 minutes - var nextBolusMins = round(3-lastBolusAge,1); + //allow SMBs every 2 minutes + var nextBolusMins = round(2-lastBolusAge,1); //console.error(naive_eventualBG, insulinReq, worstCaseInsulinReq, durationReq); - console.error("naive_eventualBG",naive_eventualBG+",",durationReq+"m zero temp needed; last bolus",lastBolusAge+"m ago; maxBolus: "+maxBolus); - if (lastBolusAge > 3) { + console.error("naive_eventualBG",naive_eventualBG+",",durationReq+"m "+smbLowTempReq+"U/h temp needed; last bolus",lastBolusAge+"m ago; maxBolus: "+maxBolus); + if (lastBolusAge > 2) { if (microBolus > 0) { rT.units = microBolus; rT.reason += "Microbolusing " + microBolus + "U. "; @@ -848,7 +898,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // if no zero temp is required, don't return yet; allow later code to set a high temp if (durationReq > 0) { - rT.rate = 0; + rT.rate = smbLowTempReq; rT.duration = durationReq; return rT; } diff --git a/lib/iob/calculate.js b/lib/iob/calculate.js index 3f8ff5059..e0de8d545 100644 --- a/lib/iob/calculate.js +++ b/lib/iob/calculate.js @@ -1,9 +1,32 @@ +var _ = require('lodash'); +var timeRef = new Date(); -function iobCalc(treatment, time, dia) { +// from: https://stackoverflow.com/a/14873282 +function erf(x) { + // save the sign of x + var sign = (x >= 0) ? 1 : -1; + x = Math.abs(x); + + // constants + var a1 = 0.254829592; + var a2 = -0.284496736; + var a3 = 1.421413741; + var a4 = -1.453152027; + var a5 = 1.061405429; + var p = 0.3275911; + + // A&S formula 7.1.26 + var t = 1.0 / (1.0 + p * x); + var y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); + return sign * y; // erf(-x) = -erf(x); +} + +// This is the old bilinear IOB curve model + +function iobCalcBiLinear(treatment, time, dia) { var diaratio = 3.0 / dia; - var peak = 75 ; - var end = 180 ; - //var sens = profile_data.sens; + var peak = 75; + var end = 180; if (typeof time === 'undefined') { time = new Date(); } @@ -12,19 +35,17 @@ function iobCalc(treatment, time, dia) { if (treatment.insulin) { var bolusTime = new Date(treatment.date); - var minAgo = diaratio * (time-bolusTime) / 1000 / 60; + var minAgo = diaratio * (time - bolusTime) / 1000 / 60; var iobContrib = 0; var activityContrib = 0; if (minAgo < peak) { - var x = (minAgo/5 + 1); + var x = (minAgo / 5 + 1); iobContrib = treatment.insulin * (1 - 0.001852 * x * x + 0.001852 * x); - //activityContrib=sens*treatment.insulin*(2/dia/60/peak)*minAgo; activityContrib = treatment.insulin * (2 / dia / 60 / peak) * minAgo; } else if (minAgo < end) { - var y = (minAgo-peak)/5; + var y = (minAgo - peak) / 5; iobContrib = treatment.insulin * (0.001323 * y * y - .054233 * y + .55556); - //activityContrib=sens*treatment.insulin*(2/dia/60-(minAgo-peak)*2/dia/60/(60*dia-peak)); activityContrib = treatment.insulin * (2 / dia / 60 - (minAgo - peak) * 2 / dia / 60 / (60 * 3 - peak)); } @@ -37,4 +58,99 @@ function iobCalc(treatment, time, dia) { return results; } + +function iobCalc(treatment, time, dia, profile) { + + if (!treatment.insulin) return {}; + + var curve = 'bilinear'; + + if (profile.curve !== undefined) { + curve = profile.curve.toLowerCase(); + } + + var curveDefaults = { + 'bilinear': { + requireLongDia: false + }, + 'rapid-acting': { + requireLongDia: true, + peak: 75, + tdMin: 300 + }, + 'ultra-rapid': { + requireLongDia: true, + peak: 55, + tdMin: 300 + }, + }; + + 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 = 'bilinear'; + } + + if (curve == 'bilinear') { + return iobCalcBiLinear(treatment, time, dia); + } + + var defaults = curveDefaults[curve]; + + var usePeakTime = false; + + var td = dia * 60; + + 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.'); + td = 300; + } + + var tp = defaults.peak; + + if (profile.useCustomPeakTime && profile.insulinPeakTime !== undefined) { + if (profile.insulinPeakTime < 35 || profile.insulinPeakTime > 120) { + console.error('Insulin Peak Time is only supported for values between 35 to 120 minutes'); + + } else { + tp = profile.insulinPeakTime; + } + } + + if (typeof time === 'undefined') { + time = timeRef; + } + + var bolusTime = new Date(treatment.date); + var t = Math.round((time - bolusTime) / 1000 / 60); + + var activityContrib = 0; + var iobContrib = 0; + var biobContrib = 0; + + // force the IOB to 0 if over 5 hours have passed + if (t < td) { + + var tau = tp * (1 - tp / td) / (1 - 2 * tp / td); + var a = 2 * tau / td; + var S = 1 / (1 - a + (1 + a) * Math.exp(-td / tau)); + + activityContrib = treatment.insulin * (S / Math.pow(tau, 2)) * t * (1 - t / td) * Math.exp(-t / tau); + iobContrib = treatment.insulin * (1 - S * (1 - a) * ((Math.pow(t, 2) / (tau * td * (1 - a)) - t / tau - 1) * Math.exp(-t / tau) + 1)); + // calculate bolus snooze insulin in the same pass + t = t * profile.bolussnooze_dia_divisor; + biobContrib = treatment.insulin * (1 - S * (1 - a) * ((Math.pow(t, 2) / (tau * td * (1 - a)) - t / tau - 1) * Math.exp(-t / tau) + 1)); + + //console.error('DIA: ' + dia + ' t: ' + t + ' td: ' + td + ' tp: ' + tp + ' tau: ' + tau + ' a: ' + a + ' S: ' + S + ' activityContrib: ' + activityContrib + ' iobContrib: ' + iobContrib); + + } + + results = { + iobContrib: iobContrib, + activityContrib: activityContrib, + biobContrib: biobContrib + }; + + return results; +} + exports = module.exports = iobCalc; diff --git a/lib/iob/index.js b/lib/iob/index.js index a58806f87..58ed3563c 100644 --- a/lib/iob/index.js +++ b/lib/iob/index.js @@ -37,8 +37,8 @@ function generate (inputs, currentIOBOnly, treatments) { // for COB calculation, we only need the zeroth element of iobArray iStop=1 } else { - // predict IOB out to DIA plus 30m to account for any running temp basals - iStop=(inputs.profile.dia*60)+30; + // predict IOB out to 4h, regardless of DIA + iStop=4*60; } for (i=0; i profile_data.current_basal/1.5 && treatment.started_at) { + // only snooze for boluses that deliver more than maxSMBBasalMinutes m worth of basal (excludes SMBs) + if (treatment.insulin > maxSMB && treatment.started_at) { //default bolussnooze_dia_divisor is 2, for 2x speed bolus snooze // bIOB = bolus IOB - var bIOB = iobCalc(treatment, time, dia / profile_data.bolussnooze_dia_divisor); - //console.log(treatment); - //console.log(bIOB); - if (bIOB && bIOB.iobContrib) bolussnooze += bIOB.iobContrib; + if (tIOB.biobContrib !== undefined) { + bolussnooze += tIOB.biobContrib; + } else { + var bIOB = iobCalc(treatment, time, dia / profile_data.bolussnooze_dia_divisor, profile_data); + //console.log(treatment); + //console.log(bIOB); + if (bIOB && bIOB.iobContrib) bolussnooze += bIOB.iobContrib; + } } else { // track microBolus IOB, but also count it toward basaliob and hightempinsulin - if (treatment.insulin <= profile_data.current_basal/1.5 && treatment.started_at) { + if (treatment.insulin <= maxSMB && treatment.started_at) { if(treatment.date > dia_ago && treatment.date <= now) { microBolusInsulin += treatment.insulin; } @@ -47,7 +61,7 @@ function iobTotal(opts, time) { } } // aIOB = basal IOB - var aIOB = iobCalc(treatment, time, dia); + var aIOB = tIOB; // iobCalc(treatment, time, dia, profile_data); if (aIOB && aIOB.iobContrib) basaliob += aIOB.iobContrib; if (treatment.insulin) { if(treatment.date > dia_ago && treatment.date <= now) { @@ -61,18 +75,19 @@ function iobTotal(opts, time) { } }); - return { - iob: Math.round( iob * 1000 ) / 1000, - activity: Math.round( activity * 10000 ) / 10000, - bolussnooze: Math.round( bolussnooze * 1000 ) / 1000, - basaliob: Math.round( basaliob * 1000 ) / 1000, - netbasalinsulin: Math.round( netbasalinsulin * 1000 ) / 1000, - hightempinsulin: Math.round( hightempinsulin * 1000 ) / 1000, - microBolusInsulin: Math.round( microBolusInsulin * 1000 ) / 1000, - microBolusIOB: Math.round( microBolusIOB * 1000 ) / 1000, + var rval = { + iob: Math.round(iob * 1000) / 1000, + activity: Math.round(activity * 10000) / 10000, + bolussnooze: Math.round(bolussnooze * 1000) / 1000, + basaliob: Math.round(basaliob * 1000) / 1000, + netbasalinsulin: Math.round(netbasalinsulin * 1000) / 1000, + hightempinsulin: Math.round(hightempinsulin * 1000) / 1000, + microBolusInsulin: Math.round(microBolusInsulin * 1000) / 1000, + microBolusIOB: Math.round(microBolusIOB * 1000) / 1000, time: time }; + + return rval; } exports = module.exports = iobTotal; - diff --git a/lib/meal/total.js b/lib/meal/total.js index 739b3249c..b1596e21a 100644 --- a/lib/meal/total.js +++ b/lib/meal/total.js @@ -1,14 +1,13 @@ var tz = require('moment-timezone'); -var calcMealCOB = require('oref0/lib/determine-basal/cob-autosens'); +var calcMealCOB = require('oref0/lib/determine-basal/cob'); -function diaCarbs(opts, time) { +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 boluses = 0; var carbDelay = 20 * 60 * 1000; var maxCarbs = 0; var mealCarbTime = time.getTime(); @@ -35,6 +34,7 @@ function diaCarbs(opts, time) { return bDate.getTime() - aDate.getTime(); }); + var carbsToRemove = 0; treatments.forEach(function(treatment) { var now = time.getTime(); // consider carbs from up to 6 hours ago in calculating COB @@ -49,13 +49,19 @@ function diaCarbs(opts, time) { var myCarbsAbsorbed = calcMealCOB(COB_inputs).carbsAbsorbed; var myMealCOB = Math.max(0, carbs - myCarbsAbsorbed); mealCOB = Math.max(mealCOB, myMealCOB); + //console.error(myMealCOB, mealCOB, carbs); + if (myMealCOB < mealCOB) { + carbsToRemove += parseFloat(treatment.carbs); + } else { + carbsToRemove = 0; + } + //console.error(carbs, carbsToRemove); //console.error("COB:",mealCOB); } - if (treatment.bolus >= 0.1) { - boluses += parseFloat(treatment.bolus); - } } }); + // only include carbs actually used in calculating COB + carbs -= carbsToRemove; // calculate the current deviation and steepest deviation downslope over the last hour COB_inputs.ciTime = time.getTime(); @@ -80,7 +86,6 @@ function diaCarbs(opts, time) { return { carbs: Math.round( carbs * 1000 ) / 1000 - , boluses: Math.round( boluses * 1000 ) / 1000 , mealCOB: Math.round( mealCOB ) , currentDeviation: Math.round( c.currentDeviation * 100 ) / 100 , maxDeviation: Math.round( c.maxDeviation * 100 ) / 100 @@ -88,5 +93,5 @@ function diaCarbs(opts, time) { }; } -exports = module.exports = diaCarbs; +exports = module.exports = recentCarbs; diff --git a/lib/oref0-setup/report.json b/lib/oref0-setup/report.json index 9284cccd1..46913656d 100644 --- a/lib/oref0-setup/report.json +++ b/lib/oref0-setup/report.json @@ -18,7 +18,7 @@ "use": "shell", "reporter": "JSON", "device": "ns", - "remainder": "-18hours", + "remainder": "-36hours", "json_default": "True" }, "name": "monitor/carbhistory.json" @@ -244,7 +244,7 @@ "json_default": "True", "pumphistory": "settings/pumphistory-24h-zoned.json", "device": "detect-sensitivity", - "remainder": "", + "remainder": "monitor/carbhistory.json", "isf": "settings/insulin_sensitivities.json", "glucose": "monitor/glucose.json" }, @@ -382,12 +382,12 @@ { "type": "report", "name": "xdrip/glucose.json", - "xdrip/glucose.json": { - "device": "xdrip", - "remainder": "", - "use": "shell", + "xdrip/glucose.json": { + "device": "xdrip", + "remainder": "", + "use": "shell", "json_default": "True", - "reporter": "text" + "reporter": "text" } } ] diff --git a/lib/profile/index.js b/lib/profile/index.js index 73789eaec..ee3d3b2b1 100644 --- a/lib/profile/index.js +++ b/lib/profile/index.js @@ -34,6 +34,11 @@ function defaults ( ) { , enableSMB_with_COB: false // enable supermicrobolus while COB is positive , enableSMB_with_temptarget: false // enable supermicrobolus for eating soon temp targets , enableSMB_after_carbs: false // enable supermicrobolus for 6h after carbs, even with 0 COB + , maxSMBBasalMinutes: 30 // maximum minutes of basal that can be delivered as a single SMB with uncovered COB + , curve: "bilinear" + , useCustomPeakTime: false + , insulinPeakTime: 75 + , carbsReqThreshold: 1 }; return profile; } diff --git a/lib/templates/exported-loop.json b/lib/templates/exported-loop.json deleted file mode 100644 index c7bc92b59..000000000 --- a/lib/templates/exported-loop.json +++ /dev/null @@ -1,679 +0,0 @@ -[ - { - "type": "alias", - "name": "rm-warmup", - "rm-warmup": { - "command": "! bash -c \"rm -f model.json monitor/clock.json > /dev/null\"" - } - }, - { - "type": "alias", - "name": "warmup", - "warmup": { - "command": "report invoke model.json raw-pump/clock-raw.json monitor/clock.json" - } - }, - { - "fail-warmup": { - "command": "! bash -c \"echo PREFLIGHT FAIL; exit 1\"" - }, - "type": "alias", - "name": "fail-warmup" - }, - { - "type": "alias", - "preflight": { - "command": "! bash -c \"(openaps rm-warmup; echo PREFLIGHT ) && openaps warmup 2>&1 >/dev/null && grep -q T monitor/clock.json && echo PREFLIGHT OK || openaps fail-warmup\"" - }, - "name": "preflight" - }, - { - "type": "alias", - "name": "monitor-cgm", - "monitor-cgm": { - "command": "report invoke monitor/glucose-raw.json monitor/glucose.json" - } - }, - { - "type": "alias", - "name": "monitor-pump-history", - "monitor-pump-history": { - "command": "report invoke raw-pump/pump-history-raw.json monitor/pump-history.json" - } - }, - { - "type": "alias", - "name": "get-basal-status", - "get-basal-status": { - "command": "report invoke monitor/temp-basal-status.json" - } - }, - { - "type": "alias", - "name": "get-pump-details", - "get-pump-details": { - "command": "report invoke monitor/reservoir.json monitor/status.json monitor/battery.json" - } - }, - { - "type": "alias", - "name": "get-settings", - "get-settings": { - "command": "report invoke raw-pump/bg-targets-raw.json settings/bg-targets.json raw-pump/insulin-sensitivities-raw.json settings/insulin-sensitivities.json raw-pump/selected-basal-profile.json settings/selected-basal-profile.json raw-pump/settings.json settings/settings.json" - } - }, - { - "type": "alias", - "name": "gather-pump-data", - "gather-pump-data": { - "command": "! bash -c \"openaps get-basal-status; openaps get-pump-details; openaps monitor-pump-history; openaps get-settings\"" - } - }, - { - "type": "alias", - "name": "gather-clean-data", - "gather-clean-data": { - "command": "! bash -c \"openaps monitor-cgm && openaps gather-pump-data\"" - } - }, - { - "type": "alias", - "name": "do-oref0", - "do-oref0": { - "command": "report invoke oref0-monitor/profile.json oref0-monitor/iob.json oref0-predict/oref0.json" - } - }, - { - "type": "alias", - "name": "enact-oref0", - "enact-oref0": { - "command": "report invoke oref0-enacted/enacted-temp-basal.json" - } - }, - { - "do-everything": { - "command": "! bash -c \"openaps preflight && openaps gather-clean-data && openaps do-oref0 && openaps enact-oref0\"" - }, - "type": "alias", - "name": "do-everything" - }, - { - "type": "vendor", - "name": "mmeowlink.vendors.mmeowlink", - "mmeowlink.vendors.mmeowlink": { - "path": ".", - "module": "mmeowlink.vendors.mmeowlink" - } - }, - { - "type": "vendor", - "name": "openxshareble", - "openxshareble": { - "path": ".", - "module": "openxshareble" - } - }, - { - "openapscontrib.timezones": { - "path": ".", - "module": "openapscontrib.timezones" - }, - "type": "vendor", - "name": "openapscontrib.timezones" - }, - { - "openapscontrib.mmhistorytools": { - "path": ".", - "module": "openapscontrib.mmhistorytools" - }, - "type": "vendor", - "name": "openapscontrib.mmhistorytools" - }, - { - "pancreabble": { - "path": ".", - "module": "pancreabble" - }, - "type": "vendor", - "name": "pancreabble" - }, - { - "main": { - "phases": "", - "rrule": "RRULE:FREQ=MINUTELY;INTERVAL=1" - }, - "type": "schedule", - "name": "main" - }, - { - "do-everything": { - "phases": "", - "rrule": "RRULE:FREQ=MINUTELY;INTERVAL=1" - }, - "type": "schedule", - "name": "do-everything" - }, - { - "pump": { - "vendor": "mmeowlink.vendors.mmeowlink", - "extra": "pump.ini" - }, - "type": "device", - "name": "pump", - "extra": { - "serial": "000000", - "radio_type": "subg_rfspy", - "port": "/dev/serial/by-id/usb-Nightscout_subg_rfspy_000002-if00" - } - }, - { - "extra": { - "fields": "", - "cmd": "oref0", - "args": "" - }, - "type": "device", - "name": "oref0", - "oref0": { - "vendor": "openaps.vendors.process", - "extra": "oref0.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" - } - }, - { - "extra": { - "fields": "settings bg-targets insulin-sensitivities basal-profile max-iob", - "cmd": "oref0", - "args": "get-profile" - }, - "type": "device", - "name": "get-profile", - "get-profile": { - "vendor": "openaps.vendors.process", - "extra": "get-profile.ini" - } - }, - { - "type": "device", - "calculate-iob": { - "vendor": "openaps.vendors.process", - "extra": "calculate-iob.ini" - }, - "name": "calculate-iob", - "extra": { - "fields": "pump-history oref0-profile clock", - "cmd": "oref0", - "args": "calculate-iob" - } - }, - { - "determine-basal": { - "vendor": "openaps.vendors.process", - "extra": "determine-basal.ini" - }, - "type": "device", - "name": "determine-basal", - "extra": { - "fields": "oref0-iob temp-basal glucose oref0-profile", - "cmd": "oref0", - "args": "determine-basal" - } - }, - { - "type": "device", - "tz": { - "vendor": "openapscontrib.timezones", - "extra": "tz.ini" - }, - "name": "tz", - "extra": {} - }, - { - "units": { - "vendor": "openaps.vendors.units", - "extra": "units.ini" - }, - "type": "device", - "name": "units", - "extra": {} - }, - { - "extra": {}, - "type": "device", - "name": "mmhistorytools", - "mmhistorytools": { - "vendor": "openapscontrib.mmhistorytools", - "extra": "mmhistorytools.ini" - } - }, - { - "type": "device", - "cgm": { - "vendor": "openxshareble", - "extra": "cgm.ini" - }, - "name": "cgm", - "extra": { - "serial": "SM12345678" - } - }, - { - "pong": { - "vendor": "openaps.vendors.process", - "extra": "pong.ini" - }, - "type": "device", - "name": "pong", - "extra": { - "fields": "thing", - "cmd": "echo", - "args": "" - } - }, - { - "ns": { - "vendor": "openaps.vendors.process", - "extra": "ns.ini" - }, - "type": "device", - "name": "ns", - "extra": { - "fields": "oper", - "cmd": "nightscout", - "args": "ns myhost.com 143505794bdd283f7daed50f42d201b6926892c8" - } - }, - { - "pebble": { - "vendor": "pancreabble", - "extra": "pebble.ini" - }, - "type": "device", - "name": "pebble", - "extra": {} - }, - { - "DoPing": { - "then": "" - }, - "type": "trigger", - "name": "DoPing" - }, - { - "type": "trigger", - "ping": { - "then": "pong" - }, - "name": "ping" - }, - { - "pong": { - "then": "" - }, - "type": "trigger", - "name": "pong" - }, - { - "settings/settings.json": { - "device": "pump", - "use": "read_settings", - "reporter": "JSON" - }, - "type": "report", - "name": "settings/settings.json" - }, - { - "type": "report", - "name": "settings/bg-targets-raw.json", - "settings/bg-targets-raw.json": { - "device": "pump", - "use": "read_bg_targets", - "reporter": "JSON" - } - }, - { - "settings/bg-targets.json": { - "device": "units", - "to": "mg/dL", - "use": "bg_targets", - "input": "raw-pump/bg-targets-raw.json", - "reporter": "JSON" - }, - "type": "report", - "name": "settings/bg-targets.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": "settings/insulin-sensitivities.json", - "settings/insulin-sensitivities.json": { - "device": "units", - "to": "mg/dL", - "use": "insulin_sensitivities", - "input": "raw-pump/insulin-sensitivities-raw.json", - "reporter": "JSON" - } - }, - { - "type": "report", - "name": "settings/selected-basal-profile.json", - "settings/selected-basal-profile.json": { - "device": "pump", - "use": "read_selected_basal_profile", - "reporter": "JSON" - } - }, - { - "monitor/clock-raw.json": { - "device": "pump", - "use": "read_clock", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/clock-raw.json" - }, - { - "monitor/clock.json": { - "use": "clock", - "reporter": "JSON", - "astimezone": "False", - "date": "None", - "adjust": "missing", - "timezone": "PST", - "device": "tz", - "input": "raw-pump/clock-raw.json" - }, - "type": "report", - "name": "monitor/clock.json" - }, - { - "monitor/temp-basal-status.json": { - "device": "pump", - "use": "read_temp_basal", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/temp-basal-status.json" - }, - { - "monitor/pump-history-raw.json": { - "hours": "8.0", - "device": "pump", - "use": "iter_pump_hours", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/pump-history-raw.json" - }, - { - "monitor/pump-history.json": { - "use": "rezone", - "reporter": "JSON", - "astimezone": "False", - "date": "timestamp dateString start_at end_at created_at", - "adjust": "missing", - "timezone": "PST", - "device": "tz", - "input": "raw-pump/pump-history-raw.json" - }, - "type": "report", - "name": "monitor/pump-history.json" - }, - { - "type": "report", - "name": "model.json", - "model.json": { - "device": "pump", - "use": "model", - "reporter": "JSON" - } - }, - { - "monitor/reservoir.json": { - "device": "pump", - "use": "reservoir", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/reservoir.json" - }, - { - "type": "report", - "name": "monitor/status.json", - "monitor/status.json": { - "device": "pump", - "use": "read_status", - "reporter": "JSON" - } - }, - { - "monitor/battery.json": { - "device": "pump", - "use": "read_battery_status", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/battery.json" - }, - { - "type": "report", - "name": "oref0-monitor/profile.json", - "oref0-monitor/profile.json": { - "insulin-sensitivities": "settings/insulin-sensitivities.json", - "use": "shell", - "settings": "settings/settings.json", - "reporter": "text", - "json_default": "True", - "device": "get-profile", - "bg-targets": "settings/bg-targets.json", - "basal-profile": "settings/selected-basal-profile.json", - "max-iob": "max-iob.json", - "remainder": "" - } - }, - { - "type": "report", - "name": "oref0-monitor/iob.json", - "oref0-monitor/iob.json": { - "use": "shell", - "clock": "monitor/clock.json", - "reporter": "text", - "json_default": "True", - "pump-history": "monitor/pump-history.json", - "oref0-profile": "oref0-monitor/profile.json", - "device": "calculate-iob", - "remainder": "" - } - }, - { - "type": "report", - "name": "oref0-predict/oref0.json", - "oref0-predict/oref0.json": { - "use": "shell", - "oref0-iob": "oref0-monitor/iob.json", - "temp-basal": "monitor/temp-basal-status.json", - "reporter": "text", - "json_default": "True", - "oref0-profile": "oref0-monitor/profile.json", - "device": "determine-basal", - "remainder": "", - "glucose": "monitor/glucose.json" - } - }, - { - "type": "report", - "name": "oref0-enacted/enacted-temp-basal.json", - "oref0-enacted/enacted-temp-basal.json": { - "device": "pump", - "input": "oref0-predict/oref0.json", - "use": "set_temp_basal", - "reporter": "JSON" - } - }, - { - "monitor/glucose-raw.json": { - "count": "20", - "device": "cgm", - "use": "iter_glucose", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/glucose-raw.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", - "adjust": "missing", - "timezone": "PST", - "device": "tz", - "input": "raw-cgm/glucose-raw.json" - } - }, - { - "cgm-vendor.json": { - "device": "cgm", - "use": "GetFirmwareHeader", - "reporter": "JSON" - }, - "type": "report", - "name": "cgm-vendor.json" - }, - { - "blah.txt": { - "thing": "foo", - "use": "shell", - "reporter": "text", - "device": "pong", - "remainder": "bar", - "json_default": "False" - }, - "type": "report", - "name": "blah.txt" - }, - { - "raw-pump/settings.json": { - "device": "pump", - "use": "read_settings", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/settings.json" - }, - { - "type": "report", - "name": "raw-pump/bg-targets-raw.json", - "raw-pump/bg-targets-raw.json": { - "device": "pump", - "use": "read_bg_targets", - "reporter": "JSON" - } - }, - { - "raw-pump/insulin-sensitivities-raw.json": { - "device": "pump", - "use": "read_insulin_sensitivities", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/insulin-sensitivities-raw.json" - }, - { - "type": "report", - "name": "raw-pump/selected-basal-profile.json", - "raw-pump/selected-basal-profile.json": { - "device": "pump", - "use": "read_selected_basal_profile", - "reporter": "JSON" - } - }, - { - "raw-pump/clock-raw.json": { - "device": "pump", - "use": "read_clock", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/clock-raw.json" - }, - { - "raw-pump/temp-basal-status.json": { - "device": "pump", - "use": "read_temp_basal", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/temp-basal-status.json" - }, - { - "raw-pump/pump-history-raw.json": { - "hours": "8.0", - "device": "pump", - "use": "iter_pump_hours", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/pump-history-raw.json" - }, - { - "raw-pump/reservoir.json": { - "device": "pump", - "use": "reservoir", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/reservoir.json" - }, - { - "type": "report", - "name": "raw-pump/status.json", - "raw-pump/status.json": { - "device": "pump", - "use": "read_status", - "reporter": "JSON" - } - }, - { - "raw-pump/battery.json": { - "device": "pump", - "use": "read_battery_status", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/battery.json" - }, - { - "type": "report", - "name": "raw-cgm/glucose-raw.json", - "raw-cgm/glucose-raw.json": { - "count": "20", - "device": "cgm", - "use": "iter_glucose", - "reporter": "JSON" - } - } -] diff --git a/lib/templates/index.js b/lib/templates/index.js deleted file mode 100644 index 16009ad65..000000000 --- a/lib/templates/index.js +++ /dev/null @@ -1,115 +0,0 @@ - - -exports.xyzbuilder = { - foo: { - default: 'ok' - }, - baz: { - default: 'optional' - } - -}; - -var reference = require('./exported-loop.json'); - - -function oref0_reports (argv) { - var reference = require('./some-reports'); - var devs = [ ]; - if (argv.oref0) { - reference.forEach(function (item) { - if (item.type == 'report') { - // if (item[item.name].device == 'pump') { - if ([null, 'get-profile', 'calculate-iob', 'determine-basal'].indexOf(item[item.name].device) > 0) { - devs.push(item); - } - } - }); - - } - console.log(JSON.stringify(devs, ' ')); -} - -function oref0_devices (argv) { - var devs = [ ]; - if (argv.oref0) { - reference.forEach(function (item) { - if (item.type == 'device') { - if (item.extra.cmd == 'oref0') { - devs.push(item); - } - } - }); - - } - console.log(JSON.stringify(devs, ' ')); -} - -function per_type (yargs) { - return yargs - .command('oref0', 'generate oref0 devices', {oref0: {default: true}}, oref0_devices) - ; -} - -function per_shape (yargs) { - return yargs - .command('oref0-inputs', 'generate reports for oref0', {oref0: {default: true}}, oref0_reports) - .command('medtronic-pump', 'organize output from medtronic pump', {name: {default: 'pump'}}, medtronic_pump_reports) - // .command('glucose', '', { }, oref0_reports) - ; -} - -function medtronic_pump_reports (argv) { - var reference = require('./medtronic-pump-reports'); - var out = [ ]; - reference.forEach(function (item) { - if (item[item.name].device == 'pump') { - if (argv.name != 'pump') { - item[item.name].device = argv.name; - } - } - out.push(item); - }); - console.log(JSON.stringify(out)); - return out; -} - -function per_alias (yargs) { - return yargs - .command('common', 'generate common aliases', {common: {default: true}}, print_aliases) -} - -function print_aliases (argv) { - var devs = [ ]; - if (argv.common) { - reference.forEach(function (item) { - if (item.type == 'alias') { - devs.push(item); - } - }); - } - console.log(JSON.stringify(devs, ' ')); -} - - -function run ( ) { -} - -exports.builder = function (yargs) { - return yargs - .command('device ', 'generate devices', per_type, run) - .command('reports ', 'generate reports', per_shape, run) - .command('alias ', 'generate aliases', per_alias, run) - .strict( ) - // .usage('$0 mint ') - // .demand('type', 1) - // .choices('type', ['devices', 'reports', 'alias', '*']) - // .options('bazbaz', { default: 'blah' }) - ; -} - -exports.handler = function (argv) { - // console.log('args', argv); - // return argv.command( - -} diff --git a/lib/templates/medtronic-pump-reports.json b/lib/templates/medtronic-pump-reports.json deleted file mode 100644 index f232030ee..000000000 --- a/lib/templates/medtronic-pump-reports.json +++ /dev/null @@ -1,233 +0,0 @@ -[ - { - "type": "report", - "name": "raw-pump/bg-targets-raw.json", - "raw-pump/bg-targets-raw.json": { - "device": "pump", - "use": "read_bg_targets", - "reporter": "JSON" - } - }, - { - "settings/bg-targets.json": { - "device": "units", - "to": "mg/dL", - "use": "bg_targets", - "input": "raw-pump/bg-targets-raw.json", - "reporter": "JSON" - }, - "type": "report", - "name": "settings/bg-targets.json" - }, - { - "raw-pump/insulin-sensitivities-raw.json": { - "device": "pump", - "use": "read_insulin_sensitivities", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/insulin-sensitivities-raw.json" - }, - { - "type": "report", - "name": "settings/insulin-sensitivities.json", - "settings/insulin-sensitivities.json": { - "device": "units", - "to": "mg/dL", - "use": "insulin_sensitivities", - "input": "raw-pump/insulin-sensitivities-raw.json", - "reporter": "JSON" - } - }, - { - "raw-pump/clock-raw.json": { - "device": "pump", - "use": "read_clock", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/clock-raw.json" - }, - { - "monitor/clock.json": { - "use": "clock", - "reporter": "JSON", - "astimezone": "False", - "date": "None", - "adjust": "missing", - "device": "tz", - "input": "raw-pump/clock-raw.json" - }, - "type": "report", - "name": "monitor/clock.json" - }, - { - "monitor/temp-basal-status.json": { - "device": "pump", - "use": "read_temp_basal", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/temp-basal-status.json" - }, - { - "type": "report", - "name": "oref0-predict/oref0.json", - "oref0-predict/oref0.json": { - "use": "shell", - "oref0-iob": "oref0-monitor/iob.json", - "temp-basal": "monitor/temp-basal-status.json", - "reporter": "text", - "json_default": "True", - "oref0-profile": "oref0-monitor/profile.json", - "device": "determine-basal", - "remainder": "", - "glucose": "monitor/glucose.json" - } - }, - { - "raw-pump/pump-history-raw.json": { - "hours": "8.0", - "device": "pump", - "use": "iter_pump_hours", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/pump-history-raw.json" - }, - { - "monitor/pump-history.json": { - "use": "rezone", - "reporter": "JSON", - "astimezone": "False", - "date": "timestamp dateString start_at end_at created_at", - "adjust": "missing", - "device": "tz", - "input": "raw-pump/pump-history-raw.json" - }, - "type": "report", - "name": "monitor/pump-history.json" - }, - { - "type": "report", - "name": "model.json", - "model.json": { - "device": "pump", - "use": "model", - "reporter": "JSON" - } - }, - { - "monitor/reservoir.json": { - "device": "pump", - "use": "reservoir", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/reservoir.json" - }, - { - "type": "report", - "name": "monitor/status.json", - "monitor/status.json": { - "device": "pump", - "use": "read_status", - "reporter": "JSON" - } - }, - { - "monitor/battery.json": { - "device": "pump", - "use": "read_battery_status", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/battery.json" - }, - { - "type": "report", - "name": "oref0-enacted/enacted-temp-basal.json", - "oref0-enacted/enacted-temp-basal.json": { - "device": "pump", - "input": "oref0-predict/oref0.json", - "use": "set_temp_basal", - "reporter": "JSON" - } - }, - { - "settings/settings.json": { - "device": "oref0", - "remainder": "copy-fresher raw-pump/settings.json", - "use": "shell", - "json_default": "True", - "reporter": "JSON" - }, - "type": "report", - "name": "settings/settings.json" - }, - { - "raw-pump/settings.json": { - "device": "pump", - "use": "read_settings", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/settings.json" - }, - { - "type": "report", - "name": "settings/selected-basal-profile.json", - "settings/selected-basal-profile.json": { - "device": "oref0", - "remainder": "copy-fresher raw-pump/selected-basal-profile.json", - "use": "shell", - "json_default": "True", - "reporter": "JSON" - } - }, - { - "type": "report", - "name": "raw-pump/selected-basal-profile.json", - "raw-pump/selected-basal-profile.json": { - "device": "pump", - "use": "read_selected_basal_profile", - "reporter": "JSON" - } - }, - { - "raw-pump/temp-basal-status.json": { - "device": "pump", - "use": "read_temp_basal", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/temp-basal-status.json" - }, - { - "raw-pump/reservoir.json": { - "device": "pump", - "use": "reservoir", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/reservoir.json" - }, - { - "type": "report", - "name": "raw-pump/status.json", - "raw-pump/status.json": { - "device": "pump", - "use": "read_status", - "reporter": "JSON" - } - }, - { - "raw-pump/battery.json": { - "device": "pump", - "use": "read_battery_status", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/battery.json" - } -] diff --git a/lib/templates/oref0-devices.json b/lib/templates/oref0-devices.json deleted file mode 100644 index ec63d730d..000000000 --- a/lib/templates/oref0-devices.json +++ /dev/null @@ -1,54 +0,0 @@ -[ - { - "extra": { - "fields": "", - "cmd": "oref0", - "args": "" - }, - "type": "device", - "name": "oref0", - "oref0": { - "vendor": "openaps.vendors.process", - "extra": "oref0.ini" - } - }, - { - "extra": { - "fields": "settings bg-targets insulin-sensitivities basal-profile max-iob", - "cmd": "oref0", - "args": "get-profile" - }, - "type": "device", - "name": "get-profile", - "get-profile": { - "vendor": "openaps.vendors.process", - "extra": "get-profile.ini" - } - }, - { - "type": "device", - "calculate-iob": { - "vendor": "openaps.vendors.process", - "extra": "calculate-iob.ini" - }, - "name": "calculate-iob", - "extra": { - "fields": "pump-history oref0-profile clock", - "cmd": "oref0", - "args": "calculate-iob" - } - }, - { - "determine-basal": { - "vendor": "openaps.vendors.process", - "extra": "determine-basal.ini" - }, - "type": "device", - "name": "determine-basal", - "extra": { - "fields": "oref0-iob temp-basal glucose oref0-profile", - "cmd": "oref0", - "args": "determine-basal" - } - } -] diff --git a/lib/templates/refresh-loops.json b/lib/templates/refresh-loops.json deleted file mode 100644 index f9d0eb8a7..000000000 --- a/lib/templates/refresh-loops.json +++ /dev/null @@ -1,721 +0,0 @@ -[ - { - "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" - } - }, - { - "type": "report", - "name": "monitor/cgm-glucose.json", - "monitor/cgm-glucose.json": { - "hours": "25.0", - "device": "cgm", - "use": "iter_glucose_hours", - "reporter": "JSON" - } - }, - { - "type": "report", - "name": "raw-cgm/raw-entries.json", - "raw-cgm/raw-entries.json": { - "count": "", - "use": "oref0_glucose", - "no_raw": "False", - "reporter": "JSON", - "seconds": "", - "minutes": "", - "hours": "25", - "device": "cgm", - "gaps": "", - "microseconds": "", - "threshold": "100", - "sensor": "", - "date": "display_time", - "glucose": "" - } - }, - { - "cgm/ns-glucose.json": { - "device": "ns-glucose", - "remainder": "", - "use": "shell", - "json_default": "True", - "reporter": "text" - }, - "type": "report", - "name": "cgm/ns-glucose.json" - }, - { - "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": "5.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-zoned.json", - "device": "iob", - "remainder": "" - } - }, - { - "type": "report", - "name": "monitor/meal.json", - "monitor/meal.json": { - "profile": "settings/profile.json", - "carbs": "monitor/glucose.json", - "clock": "monitor/clock-zoned.json", - "reporter": "text", - "json_default": "True", - "use": "shell", - "pumphistory": "monitor/pumphistory-zoned.json", - "basal": "monitor/carbhistory.json", - "device": "meal", - "remainder": "", - "glucose": "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": "", - "isf": "settings/insulin_sensitivities.json", - "glucose": "monitor/glucose.json" - }, - "name": "settings/autosens.json" - }, - { - "type": "report", - "settings/bg_targets.json": { - "device": "pump", - "use": "read_bg_targets", - "reporter": "JSON" - }, - "name": "settings/bg_targets.json" - }, - { - "settings/insulin_sensitivities.json": { - "device": "pump", - "use": "read_insulin_sensitivities", - "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", - "isf": "settings/insulin_sensitivities.json" - }, - "type": "report", - "name": "settings/profile.json" - }, - { - "type": "report", - "name": "enact/suggested.json", - "enact/suggested.json": { - "profile": "settings/profile.json", - "use": "shell", - "temp_basal": "monitor/temp_basal.json", - "reporter": "text", - "json_default": "True", - "meal": "monitor/meal.json", - "autosens": "settings/autosens.json", - "device": "determine-basal", - "remainder": "", - "iob": "monitor/iob.json", - "glucose": "monitor/glucose.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": "device", - "cgm": { - "vendor": "openaps.vendors.dexcom", - "extra": "cgm.ini" - }, - "name": "cgm", - "extra": {} - }, - { - "ns-glucose": { - "vendor": "openaps.vendors.process", - "extra": "ns-glucose.ini" - }, - "type": "device", - "name": "ns-glucose", - "extra": { - "fields": "", - "cmd": "bash -c \"curl --compressed -m 30 -s $NIGHTSCOUT_HOST/api/v1/entries/sgv.json?count=1000 | json -e \\\"this.glucose = this.sgv\\\"\"", - "args": "" - } - }, - { - "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 carbs glucose basal", - "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" - } - }, - { - "determine-basal": { - "vendor": "openaps.vendors.process", - "extra": "determine-basal.ini" - }, - "type": "device", - "name": "determine-basal", - "extra": { - "fields": "iob temp_basal glucose profile autosens meal", - "cmd": "oref0", - "args": "determine-basal" - } - }, - { - "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": {} - }, - { - "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" - } - }, - { - "type": "alias", - "name": "invoke", - "invoke": { - "command": "report invoke" - } - }, - { - "type": "alias", - "name": "mmtune", - "mmtune": { - "command": "! bash -c \"echo -n \\\"mmtune: \\\" && openaps report invoke monitor/mmtune.json 2>/dev/null >/dev/null; grep -v setFreq monitor/mmtune.json | grep -A2 $(json -a setFreq -f monitor/mmtune.json) | while read line; do echo -n \\\"$line \\\"; done\"" - } - }, - { - "type": "alias", - "name": "wait-for-silence", - "wait-for-silence": { - "command": "! bash -c \"echo -n \\\"Listening: \\\"; for i in $(seq 1 100); do echo -n .; ~/src/mmeowlink/bin/mmeowlink-any-pump-comms.py --port /dev/mmeowlink --wait-for 30 2>/dev/null | egrep -v subg | egrep No && break; done\"" - } - }, - { - "wait-for-long-silence": { - "command": "! bash -c \"echo -n \\\"Listening: \\\"; for i in $(seq 1 100); do echo -n .; ~/src/mmeowlink/bin/mmeowlink-any-pump-comms.py --port /dev/mmeowlink --wait-for 45 2>/dev/null | egrep -v subg | egrep No && break; done\"" - }, - "type": "alias", - "name": "wait-for-long-silence" - }, - { - "type": "alias", - "name": "monitor-cgm", - "monitor-cgm": { - "command": "report invoke raw-cgm/raw-entries.json cgm/cgm-glucose.json" - } - }, - { - "type": "alias", - "name": "get-ns-glucose", - "get-ns-glucose": { - "command": "report invoke cgm/ns-glucose.json" - } - }, - { - "monitor-pump": { - "command": "report invoke monitor/clock.json monitor/temp_basal.json monitor/pumphistory.json monitor/pumphistory-zoned.json monitor/clock-zoned.json monitor/iob.json monitor/meal.json monitor/reservoir.json monitor/battery.json monitor/status.json" - }, - "type": "alias", - "name": "monitor-pump" - }, - { - "type": "alias", - "ns-temptargets": { - "command": "! bash -c \"curl --compressed -m 30 -s \\\"$NIGHTSCOUT_HOST/api/v1/treatments.json?find\\[created_at\\]\\[\\$gte\\]=$(date -d \\\"6 hours ago\\\" -Iminutes)&find\\[eventType\\]\\[\\$regex\\]=Target\\\" > settings/temptargets.json; openaps report invoke settings/profile.json 2>/dev/null >/dev/null; exit 0 \"" - }, - "name": "ns-temptargets" - }, - { - "ns-meal-carbs": { - "command": "! bash -c \"curl --compressed -m 30 -s \\\"$NIGHTSCOUT_HOST/api/v1/treatments.json?find\\[carbs\\]\\[\\$exists\\]=true\\\" > monitor/carbhistory.json; oref0-meal monitor/pumphistory-zoned.json settings/profile.json monitor/clock-zoned.json monitor/carbhistory.json monitor/glucose.json settings/basal_profile.json > monitor/meal.json.new; grep -q COB monitor/meal.json.new && mv monitor/meal.json.new monitor/meal.json; exit 0\"" - }, - "type": "alias", - "name": "ns-meal-carbs" - }, - { - "type": "alias", - "name": "get-settings", - "get-settings": { - "command": "report invoke settings/model.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json settings/settings.json settings/carb_ratios.json settings/profile.json" - } - }, - { - "type": "alias", - "name": "bg-fresh-check", - "bg-fresh-check": { - "command": "! bash -c \"cat cgm/glucose.json | json -c \\\"minAgo=(new Date()-new Date(this.dateString))/60/1000; return minAgo < 6 && minAgo > 0 && this.glucose > 38\\\" | grep -q glucose\"" - } - }, - { - "get-bg": { - "command": "! bash -c \"openaps monitor-cgm 2>/dev/null | tail -1 && grep -q glucose cgm/cgm-glucose.json && cp -pu cgm/cgm-glucose.json cgm/glucose.json; cp -pu cgm/glucose.json monitor/glucose.json\"" - }, - "type": "alias", - "name": "get-bg" - }, - { - "type": "alias", - "name": "get-ns-bg", - "get-ns-bg": { - "command": "! bash -c \"openaps get-ns-glucose && cat cgm/ns-glucose.json | json -c \\\"minAgo=(new Date()-new Date(this.dateString))/60/1000; return minAgo < 10 && minAgo > -5 && this.glucose > 38\\\" | grep -q glucose && cp -pu cgm/ns-glucose.json cgm/glucose.json; cp -pu cgm/glucose.json monitor/glucose.json\"" - } - }, - { - "gather": { - "command": "! bash -c \"openaps report invoke monitor/status.json 2>/dev/null >/dev/null && echo -n Ref && test $(cat monitor/status.json | json bolusing) == false && echo -n resh && ( openaps monitor-pump || openaps monitor-pump ) 2>/dev/null >/dev/null && echo ed pumphistory || (echo; exit 1) 2>/dev/null\"" - }, - "type": "alias", - "name": "gather" - }, - { - "type": "alias", - "autosens": { - "command": "! bash -c \"(find settings/ -newer settings/autosens.json | grep -q pumphistory-24h-zoned.json || find settings/ -size -5c | grep -q autosens.json || ! find settings/ | grep -q autosens || ! find settings/autosens.json) && openaps invoke settings/autosens.json\"" - }, - "name": "autosens" - }, - { - "type": "alias", - "name": "refresh-old-pumphistory", - "refresh-old-pumphistory": { - "command": "! bash -c \"find monitor/ -mmin -15 -size +100c | grep -q pumphistory-zoned || ( echo -n \\\"Old pumphistory: \\\" && openaps gather && openaps enact ) \"" - } - }, - { - "refresh-old-pumphistory-24h": { - "command": "! bash -c \"find settings/ -mmin -120 -size +100c | grep -q pumphistory-24h-zoned || (echo -n Old pumphistory-24h refresh && openaps report invoke settings/pumphistory-24h.json settings/pumphistory-24h-zoned.json 2>/dev/null >/dev/null && echo ed)\"" - }, - "type": "alias", - "name": "refresh-old-pumphistory-24h" - }, - { - "refresh-temp-and-enact": { - "command": "! bash -c \"if( (find monitor/ -newer monitor/temp_basal.json | grep -q glucose.json && echo glucose.json newer than temp_basal.json ) || (! find monitor/ -mmin -5 -size +5c | grep -q temp_basal && echo temp_basal.json more than 5m old)); then (echo -n Temp refresh && openaps report invoke monitor/temp_basal.json monitor/clock.json monitor/clock-zoned.json monitor/iob.json 2>/dev/null >/dev/null && echo ed && if(cat monitor/temp_basal.json | json -c \\\"this.duration < 27\\\" | grep -q duration); then openaps enact; else echo Temp duration 27m or more; fi); else echo temp_basal.json less than 5m old; fi\"" - }, - "type": "alias", - "name": "refresh-temp-and-enact" - }, - { - "type": "alias", - "name": "refresh-pumphistory-and-enact", - "refresh-pumphistory-and-enact": { - "command": "! bash -c \"if ((find monitor/ -newer monitor/pumphistory-zoned.json | grep -q glucose.json && echo -n glucose.json newer than pumphistory) || (find enact/ -newer monitor/pumphistory-zoned.json | grep -q enacted.json && echo -n enacted.json newer than pumphistory) || (! find monitor/ -mmin -5 | grep -q pumphistory-zoned && echo -n pumphistory more than 5m old) ); then (echo -n \\\": \\\" && openaps gather && openaps enact ); else echo Pumphistory less than 5m old; fi \"" - } - }, - { - "refresh-old-profile": { - "command": "! bash -c \"find settings/ -mmin -60 -size +5c | grep -q settings/profile.json && echo Profile less than 60m old || (echo -n Old settings refresh && openaps get-settings 2>/dev/null >/dev/null && echo ed )\"" - }, - "type": "alias", - "name": "refresh-old-profile" - }, - { - "refresh-profile": { - "command": "! bash -c \"find settings/ -mmin -10 -size +5c | grep -q settings.json && echo Settings less than 10m old || (echo -n Settings refresh && openaps get-settings 2>/dev/null >/dev/null && echo ed)\"" - }, - "type": "alias", - "name": "refresh-profile" - }, - { - "refresh-pumphistory-24h": { - "command": "! bash -c \"find settings/ -mmin -20 -size +100c | grep -q pumphistory-24h-zoned && echo Pumphistory-24 less than 20m old || (echo -n pumphistory-24h refresh && openaps report invoke settings/pumphistory-24h.json settings/pumphistory-24h-zoned.json 2>/dev/null >/dev/null && echo ed)\"" - }, - "type": "alias", - "name": "refresh-pumphistory-24h" - }, - { - "merge-pumphistory-long": { - "command": "! bash -c \"jq -s \\\".[0] + .[1]|unique|sort_by(.timestamp)|reverse\\\" settings/pumphistory-long.json monitor/pumphistory-zoned.json settings/pumphistory-24h-zoned.json > pumphistory-long.json.new && mv pumphistory-long.json.new settings/pumphistory-long.json\"" - }, - "type": "alias", - "name": "merge-pumphistory-long" - }, - { - "type": "alias", - "name": "enact", - "enact": { - "command": "! bash -c \"rm enact/suggested.json; openaps invoke enact/suggested.json && if (cat enact/suggested.json | json -0 && grep -q duration enact/suggested.json); then ( rm enact/enacted.json; openaps invoke enact/enacted.json ; grep -q duration enact/enacted.json || openaps invoke enact/enacted.json ) 2>&1 | egrep -v \\\"^ |subg_rfspy|handler\\\" ; grep incorrectly enact/suggested.json && ~/src/oref0/bin/clockset.sh 2>/dev/null; cat enact/enacted.json | json -0; fi\"" - } - }, - { - "type": "alias", - "name": "ns-loop", - "ns-loop": { - "command": "! bash -c \"echo Starting ns-loop at $(date): && openaps get-ns-bg; openaps autosens; openaps ns-temptargets && echo -n Refreshed temptargets && openaps ns-meal-carbs && echo \\\" and meal-carbs\\\" && openaps upload\"" - } - }, - { - "pump-loop": { - "command": "! bash -c \"sleep $[ ( $RANDOM / 2048 ) ]s; until(echo Starting pump-loop at $(date): && openaps wait-for-silence && openaps refresh-old-pumphistory && openaps refresh-old-pumphistory-24h && openaps refresh-old-profile && openaps refresh-temp-and-enact && openaps refresh-pumphistory-and-enact && openaps refresh-profile && openaps refresh-pumphistory-24h && echo Completed pump-loop at $(date) && echo); do echo Error, retrying && [[ $RANDOM > 30000 ]] && openaps wait-for-long-silence && openaps mmtune; sleep 5; done\"" - }, - "type": "alias", - "name": "pump-loop" - }, - { - "pebble": { - "command": "! bash -c \"grep -q iob monitor/iob.json && grep -q absolute enact/suggested.json && openaps report invoke upload/pebble.json\"" - }, - "type": "alias", - "name": "pebble" - }, - { - "type": "alias", - "name": "latest-ns-treatment-time", - "latest-ns-treatment-time": { - "command": "! bash -c \"nightscout latest-openaps-treatment $NIGHTSCOUT_HOST | json created_at\"" - } - }, - { - "type": "alias", - "name": "format-latest-nightscout-treatments", - "format-latest-nightscout-treatments": { - "command": "! bash -c \"nightscout cull-latest-openaps-treatments monitor/pumphistory-zoned.json settings/model.json $(openaps latest-ns-treatment-time) > upload/latest-treatments.json\"" - } - }, - { - "type": "alias", - "name": "upload-recent-treatments", - "upload-recent-treatments": { - "command": "! bash -c \"openaps format-latest-nightscout-treatments && test $(json -f upload/latest-treatments.json -a created_at eventType | wc -l ) -gt 0 && (ns-upload $NIGHTSCOUT_HOST $API_SECRET treatments.json upload/latest-treatments.json ) || echo \\\"No recent treatments to upload\\\"\"" - } - }, - { - "type": "alias", - "name": "format-ns-status", - "format-ns-status": { - "command": "! bash -c \"ns-status monitor/clock-zoned.json monitor/iob.json enact/suggested.json enact/enacted.json monitor/battery.json monitor/reservoir.json monitor/status.json > upload/ns-status.json\"" - } - }, - { - "upload-ns-status": { - "command": "! bash -c \"grep -q iob monitor/iob.json && find enact/ -mmin -5 -size +5c | grep -q suggested.json && openaps format-ns-status && grep -q iob upload/ns-status.json && ns-upload $NIGHTSCOUT_HOST $API_SECRET devicestatus.json upload/ns-status.json\"" - }, - "type": "alias", - "name": "upload-ns-status" - }, - { - "type": "alias", - "name": "upload", - "upload": { - "command": "! bash -c \"echo -n Upload && ( openaps upload-ns-status; openaps upload-pumphistory-entries; openaps upload-recent-treatments ) 2>/dev/null >/dev/null && echo ed\"" - } - } -] diff --git a/lib/templates/some-reports.json b/lib/templates/some-reports.json deleted file mode 100644 index ab1172a59..000000000 --- a/lib/templates/some-reports.json +++ /dev/null @@ -1,320 +0,0 @@ -[ - { - "settings/settings.json": { - "device": "oref0", - "remainder": "copy-fresher raw-pump/settings.json", - "use": "shell", - "json_default": "True", - "reporter": "JSON" - }, - "type": "report", - "name": "settings/settings.json" - }, - { - "type": "report", - "name": "raw-pump/bg-targets-raw.json", - "raw-pump/bg-targets-raw.json": { - "device": "pump", - "use": "read_bg_targets", - "reporter": "JSON" - } - }, - { - "settings/bg-targets.json": { - "device": "units", - "to": "mg/dL", - "use": "bg_targets", - "input": "raw-pump/bg-targets-raw.json", - "reporter": "JSON" - }, - "type": "report", - "name": "settings/bg-targets.json" - }, - { - "raw-pump/insulin-sensitivities-raw.json": { - "device": "pump", - "use": "read_insulin_sensitivities", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/insulin-sensitivities-raw.json" - }, - { - "type": "report", - "name": "settings/insulin-sensitivities.json", - "settings/insulin-sensitivities.json": { - "device": "units", - "to": "mg/dL", - "use": "insulin_sensitivities", - "input": "raw-pump/insulin-sensitivities-raw.json", - "reporter": "JSON" - } - }, - { - "type": "report", - "name": "settings/selected-basal-profile.json", - "settings/selected-basal-profile.json": { - "device": "oref0", - "remainder": "copy-fresher raw-pump/selected-basal-profile.json", - "use": "shell", - "json_default": "True", - "reporter": "JSON" - } - }, - { - "raw-pump/clock-raw.json": { - "device": "pump", - "use": "read_clock", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/clock-raw.json" - }, - { - "monitor/clock.json": { - "use": "clock", - "reporter": "JSON", - "astimezone": "False", - "date": "None", - "adjust": "missing", - "timezone": "PDT", - "device": "tz", - "input": "raw-pump/clock-raw.json" - }, - "type": "report", - "name": "monitor/clock.json" - }, - { - "monitor/temp-basal-status.json": { - "device": "pump", - "use": "read_temp_basal", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/temp-basal-status.json" - }, - { - "raw-pump/pump-history-raw.json": { - "hours": "8.0", - "device": "pump", - "use": "iter_pump_hours", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/pump-history-raw.json" - }, - { - "monitor/pump-history.json": { - "use": "rezone", - "reporter": "JSON", - "astimezone": "False", - "date": "timestamp dateString start_at end_at created_at", - "adjust": "missing", - "timezone": "PDT", - "device": "tz", - "input": "raw-pump/pump-history-raw.json" - }, - "type": "report", - "name": "monitor/pump-history.json" - }, - { - "type": "report", - "name": "model.json", - "model.json": { - "device": "pump", - "use": "model", - "reporter": "JSON" - } - }, - { - "monitor/reservoir.json": { - "device": "pump", - "use": "reservoir", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/reservoir.json" - }, - { - "type": "report", - "name": "monitor/status.json", - "monitor/status.json": { - "device": "pump", - "use": "read_status", - "reporter": "JSON" - } - }, - { - "monitor/battery.json": { - "device": "pump", - "use": "read_battery_status", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/battery.json" - }, - { - "type": "report", - "name": "oref0-monitor/profile.json", - "oref0-monitor/profile.json": { - "insulin-sensitivities": "settings/insulin-sensitivities.json", - "use": "shell", - "settings": "settings/settings.json", - "reporter": "text", - "json_default": "True", - "device": "get-profile", - "bg-targets": "settings/bg-targets.json", - "basal-profile": "settings/selected-basal-profile.json", - "max-iob": "max-iob.json", - "remainder": "" - } - }, - { - "type": "report", - "name": "oref0-monitor/iob.json", - "oref0-monitor/iob.json": { - "use": "shell", - "clock": "monitor/clock.json", - "reporter": "text", - "json_default": "True", - "pump-history": "monitor/pump-history.json", - "oref0-profile": "oref0-monitor/profile.json", - "device": "calculate-iob", - "remainder": "" - } - }, - { - "type": "report", - "name": "oref0-predict/oref0.json", - "oref0-predict/oref0.json": { - "use": "shell", - "oref0-iob": "oref0-monitor/iob.json", - "temp-basal": "monitor/temp-basal-status.json", - "reporter": "text", - "json_default": "True", - "oref0-profile": "oref0-monitor/profile.json", - "device": "determine-basal", - "remainder": "", - "glucose": "monitor/glucose.json" - } - }, - { - "type": "report", - "name": "oref0-enacted/enacted-temp-basal.json", - "oref0-enacted/enacted-temp-basal.json": { - "device": "pump", - "input": "oref0-predict/oref0.json", - "use": "set_temp_basal", - "reporter": "JSON" - } - }, - { - "monitor/glucose-raw.json": { - "count": "20", - "device": "cgm", - "use": "iter_glucose", - "reporter": "JSON" - }, - "type": "report", - "name": "monitor/glucose-raw.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", - "adjust": "missing", - "timezone": "PDT", - "device": "tz", - "input": "raw-cgm/glucose-raw.json" - } - }, - { - "cgm-vendor.json": { - "device": "cgm", - "use": "GetFirmwareHeader", - "reporter": "JSON" - }, - "type": "report", - "name": "cgm-vendor.json" - }, - { - "blah.txt": { - "thing": "foo", - "use": "shell", - "reporter": "text", - "device": "pong", - "remainder": "bar", - "json_default": "False" - }, - "type": "report", - "name": "blah.txt" - }, - { - "raw-pump/settings.json": { - "device": "pump", - "use": "read_settings", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/settings.json" - }, - { - "type": "report", - "name": "raw-pump/selected-basal-profile.json", - "raw-pump/selected-basal-profile.json": { - "device": "pump", - "use": "read_selected_basal_profile", - "reporter": "JSON" - } - }, - { - "raw-pump/temp-basal-status.json": { - "device": "pump", - "use": "read_temp_basal", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/temp-basal-status.json" - }, - { - "raw-pump/reservoir.json": { - "device": "pump", - "use": "reservoir", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/reservoir.json" - }, - { - "type": "report", - "name": "raw-pump/status.json", - "raw-pump/status.json": { - "device": "pump", - "use": "read_status", - "reporter": "JSON" - } - }, - { - "raw-pump/battery.json": { - "device": "pump", - "use": "read_battery_status", - "reporter": "JSON" - }, - "type": "report", - "name": "raw-pump/battery.json" - }, - { - "type": "report", - "name": "raw-cgm/glucose-raw.json", - "raw-cgm/glucose-raw.json": { - "count": "20", - "device": "cgm", - "use": "iter_glucose", - "reporter": "JSON" - } - } -] diff --git a/package.json b/package.json index 5f00da705..21a513ae7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oref0", - "version": "0.5.3", + "version": "0.6.0-dev", "description": "openaps oref0 reference implementation of the reference design", "scripts": { "test": "make test", @@ -74,7 +74,6 @@ "oref0-setup": "./bin/oref0-setup.sh", "oref0-subg-ww-radio-parameters": "./bin/oref0-subg-ww-radio-parameters.sh", "oref0_subg_ww_radio_parameters.py": "./bin/oref0_subg_ww_radio_parameters.py", - "oref0-template": "./bin/oref0-template.js", "oref0-truncate-git-history": "bin/oref0-truncate-git-history.sh", "oref0-upload-profile": "./bin/oref0-upload-profile.js", "peb-urchin-status": "./bin/peb-urchin-status.sh", diff --git a/tests/determine-basal.test.js b/tests/determine-basal.test.js index 59cae5a60..d8f833a19 100644 --- a/tests/determine-basal.test.js +++ b/tests/determine-basal.test.js @@ -432,9 +432,10 @@ describe('determine-basal', function ( ) { }); it('should set lower high-temp when high and falling almost fast enough with low insulin activity', function () { - var glucose_status = {"delta":-8,"glucose":300,"long_avgdelta":-5,"short_avgdelta":-5}; + var glucose_status = {"delta":-6,"glucose":300,"long_avgdelta":-5,"short_avgdelta":-5}; var iob_data = {"iob":0.5,"activity":0.005,"bolussnooze":0}; var output = determine_basal(glucose_status, currenttemp, iob_data, profile, undefined, meal_data, tempBasalFunctions); + console.error(output); output.rate.should.be.above(1); output.rate.should.be.below(2); output.duration.should.equal(30); @@ -442,7 +443,7 @@ describe('determine-basal', function ( ) { }); it('should reduce high-temp when high and falling almost fast enough with low insulin activity', function () { - var glucose_status = {"delta":-8,"glucose":300,"long_avgdelta":-5,"short_avgdelta":-5}; + var glucose_status = {"delta":-6,"glucose":300,"long_avgdelta":-5,"short_avgdelta":-5}; var iob_data = {"iob":0.5,"activity":0.005,"bolussnooze":0}; var currenttemp = {"duration":30,"rate":3.5,"temp":"absolute"}; var output = determine_basal(glucose_status, currenttemp, iob_data, profile, undefined, meal_data, tempBasalFunctions); diff --git a/tests/iob.test.js b/tests/iob.test.js index 33075ef95..657f013d7 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -4,356 +4,970 @@ require('should'); var moment = require('moment'); -describe('IOB', function ( ) { - - it('should calculate IOB', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 1, 'minutes': 0}]; - - var now = Date.now() - , timestamp = new Date(now).toISOString() - , inputs = { - clock: timestamp - , history: [{ - _type: 'Bolus' - , amount: 1 - , timestamp: timestamp - }] - , profile: { - dia: 3, bolussnooze_dia_divisor: 2, basalprofile: basalprofile, current_basal: 1 } - - }; - - var rightAfterBolus = require('../lib/iob')(inputs)[0]; - rightAfterBolus.iob.should.equal(1); - rightAfterBolus.bolussnooze.should.equal(1); - - var hourLaterInputs = inputs; - hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - hourLater.iob.should.be.lessThan(1); - hourLater.bolussnooze.should.be.lessThan(.5); - hourLater.iob.should.be.greaterThan(0); - - var afterDIAInputs = inputs; - afterDIAInputs.clock = new Date(now + (3 * 60 * 60 * 1000)).toISOString(); - var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; - - afterDIA.iob.should.equal(0); - afterDIA.bolussnooze.should.equal(0); - }); - - it('should snooze fast if bolussnooze_dia_divisor is high', function() { - - var now = Date.now() - , timestamp = new Date(now).toISOString() - , inputs = { - clock: timestamp - , history: [{ - _type: 'Bolus' - , amount: 1 - , timestamp: timestamp - }] - , profile: { - dia: 3 - , bolussnooze_dia_divisor: 10 - } - }; - - var snoozeInputs = inputs; - snoozeInputs.clock = new Date(now + (20 * 60 * 1000)).toISOString(); - var snooze = require('../lib/iob')(snoozeInputs)[0]; - snooze.bolussnooze.should.equal(0); - }); - - it('should calculate IOB with Temp Basals', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 1, 'minutes': 0}]; - var now = Date.now() - , timestamp = new Date(now).toISOString() - , timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString() - , timestamp60mAgo = new Date(now - (60 * 60 * 1000)).toISOString() - , inputs = {clock: timestamp, - history: [{_type: 'TempBasalDuration','duration (min)': 30, date: timestamp60mAgo} - , {_type: 'TempBasal', rate: 2, date: timestamp60mAgo, timestamp: timestamp60mAgo} - , {_type: 'TempBasal', rate: 2, date: timestamp30mAgo, timestamp: timestamp30mAgo} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestamp}] - , profile: { dia: 3, current_basal: 1, bolussnooze_dia_divisor: 2, 'basalprofile': basalprofile} - }; - - var iobInputs = inputs; - iobInputs.clock = timestamp - var iobNow = require('../lib/iob')(iobInputs)[0]; - - //console.log(iobNow); - iobNow.iob.should.be.lessThan(1); - iobNow.iob.should.be.greaterThan(0.5); - }); - - it('should calculate IOB with Temp Basals and a basal profile', function() { - - var startingPoint = moment('2016-06-13 01:00:00.000'); - var timestamp = startingPoint.format(); - var timestampEarly = startingPoint.subtract(30,'minutes').format(); - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 2, 'minutes': 0}, - {'i': 1, 'start': '01:00:00', 'rate': 1, 'minutes': 60 }]; - - var inputs = {clock: timestamp, - history: [{_type: 'TempBasalDuration','duration (min)': 30, date: timestampEarly} - , {_type: 'TempBasal', rate: 2, date: timestampEarly, timestamp: timestampEarly} - , {_type: 'TempBasal', rate: 2, date: timestamp, timestamp: timestamp} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestamp}] - , profile: { dia: 3, bolussnooze_dia_divisor: 2, basalprofile: basalprofile} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-13 01:30:00.000'); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - hourLater.iob.should.be.lessThan(0.5); - hourLater.iob.should.be.greaterThan(0.4); - }); - - it('should calculate IOB with Temp Basals that overlap midnight and a basal profile', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 2, 'minutes': 0}, - {'i': 1, 'start': '00:15:00', 'rate': 1, 'minutes': 15 }, - {'i': 2, 'start': '00:45:00', 'rate': 0.5, 'minutes': 45 }]; - - var startingPoint = moment('2016-06-13 00:15:00.000'); - var timestamp = startingPoint.format(); - var timestampEarly = startingPoint.subtract(30,'minutes').format() - , inputs = {clock: timestamp, - history: [{_type: 'TempBasalDuration','duration (min)': 30, date: timestampEarly} - , {_type: 'TempBasal', rate: 2, date: timestampEarly, timestamp: timestampEarly} - , {_type: 'TempBasal', rate: 2, date: timestamp, timestamp: timestamp} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestamp}] - , profile: { dia: 3, current_basal: 0.1, bolussnooze_dia_divisor: 2, basalprofile: basalprofile} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-13 00:45:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.be.lessThan(0.8); - hourLater.iob.should.be.greaterThan(0.7); - }); - - it('should calculate IOB with Temp Basals that overlap each other', function() { - - var nowDate = new Date(); - var now = Date.now(); - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 1, 'minutes': 0}]; - - var startingPoint = moment('2016-06-13 00:30:00.000'); - var timestampEarly = moment('2016-06-13 00:30:00.000').subtract(30,'minutes'); - var timestampEarly2 = moment('2016-06-13 00:30:00.000').subtract(29,'minutes'); - var timestampEarly3 = moment('2016-06-13 00:30:00.000').subtract(28,'minutes'); - - var timestamp = startingPoint; - var inputs = {clock: timestamp, - history: [ - {_type: 'TempBasalDuration','duration (min)': 30, date: timestampEarly.unix()} - , {_type: 'TempBasal', rate: 2, date: timestampEarly.unix(), timestamp: timestampEarly.format()} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestampEarly2.unix()} - , {_type: 'TempBasal', rate: 2, date: timestampEarly2.unix(), timestamp: timestampEarly2.format()} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestampEarly3.unix()} - , {_type: 'TempBasal', rate: 2, date: timestampEarly3.unix(), timestamp: timestampEarly3.format()} - , {_type: 'TempBasal', rate: 2, date: timestamp.unix(), timestamp: timestamp.format()} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestamp.unix()}] - , profile: { dia: 3, current_basal: 0.1, bolussnooze_dia_divisor: 2, basalprofile: basalprofile} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-13 00:30:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.be.lessThan(0.5); - hourLater.iob.should.be.greaterThan(0.45); - }); - it('should calculate IOB with Temp Basals that overlap midnight and a basal profile, part deux', function() { - - var nowDate = new Date(); - var now = Date.now(); - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 2, 'minutes': 0}, - {'i': 1, 'start': '00:15:00', 'rate': 0.1, 'minutes': 15 }, - {'i': 1, 'start': '00:30:00', 'rate': 2, 'minutes': 30 }, - {'i': 1, 'start': '00:45:00', 'rate': 0.1, 'minutes': 45 }]; - - var startingPoint = moment('2016-06-13 23:45:00.000'); - var timestamp = startingPoint.format(); - var timestampEarly = startingPoint.subtract(30,'minutes').format() - , inputs = {clock: timestamp, - history: [{_type: 'TempBasalDuration','duration (min)': 60, date: timestamp} - , {_type: 'TempBasal', rate: 2, date: timestamp, timestamp: timestamp} ] - , profile: { dia: 3, current_basal: 0.1, bolussnooze_dia_divisor: 2, basalprofile: basalprofile} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-14 00:45:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.be.lessThan(1); - hourLater.iob.should.be.greaterThan(0.8); - }); - - - it('should not report negative IOB with Temp Basals and a basal profile with drastic changes', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 0.1, 'minutes': 0}, - {'i': 1, 'start': '00:30:00', 'rate': 2, 'minutes': 30 }]; - - var startingPoint = new Date('2016-06-13 00:00:00.000'); - var startingPoint2 = new Date('2016-06-13 00:30:00.000'); - var endPoint = new Date('2016-06-13 01:00:00.000'); - - var inputs = {clock: endPoint, - history: [{_type: 'TempBasalDuration','duration (min)': 30, date: startingPoint} - , {_type: 'TempBasal', rate: 0.1, date: startingPoint, timestamp: startingPoint} - , {_type: 'TempBasal', rate: 2, date: startingPoint2, timestamp: startingPoint2} - , {_type: 'TempBasalDuration','duration (min)': 30, date: startingPoint2}] - , profile: { dia: 3, current_basal: 2, bolussnooze_dia_divisor: 2, 'basalprofile': basalprofile} - }; - - var hourLaterInputs = inputs; - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - hourLater.iob.should.equal(0); - }); - - it('should calculate IOB with Temp Basal events that overlap', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 1, 'minutes': 0}]; - - var now = Date.now() - , timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString() - , timestamp31mAgo = new Date(now - (31 * 60 * 1000)).toISOString() - , inputs = {clock: timestamp30mAgo, - history: [{_type: 'TempBasalDuration','duration (min)': 30, date: timestamp31mAgo} - ,{_type: 'TempBasal', rate: 2, date: timestamp31mAgo, timestamp: timestamp31mAgo} - ,{_type: 'TempBasal', rate: 2, date: timestamp30mAgo, timestamp: timestamp30mAgo} - ,{_type: 'TempBasalDuration','duration (min)': 30, date: timestamp30mAgo}] - , profile: { dia: 3, current_basal: 1, 'basalprofile': basalprofile} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.be.lessThan(1); - hourLater.iob.should.be.greaterThan(0); - - }); - - it('should calculate IOB with Temp Basals that are lower than base rate', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 2, 'minutes': 0}]; - - var now = Date.now() - , timestamp = new Date(now).toISOString() - , timestampEarly = new Date(now - (30 * 60 * 1000)).toISOString() - , inputs = {clock: timestamp, - history: [{_type: 'TempBasalDuration','duration (min)': 30, date: timestampEarly} - , {_type: 'TempBasal', rate: 1, date: timestampEarly, timestamp: timestampEarly} - , {_type: 'TempBasal', rate: 1, date: timestamp, timestamp: timestamp} - , {_type: 'TempBasalDuration','duration (min)': 30, date: timestamp}] - , profile: { dia: 3, current_basal: 2, bolussnooze_dia_divisor: 2, 'basalprofile': basalprofile} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.be.lessThan(0); - hourLater.iob.should.be.greaterThan(-1); - - }); - - it('should show 0 IOB with Temp Basals if duration is not found', function() { - - var now = Date.now() - , timestamp = new Date(now).toISOString() - , timestampEarly = new Date(now - (60 * 60 * 1000)).toISOString() - , inputs = { - clock: timestamp - , history: [{_type: 'TempBasal', rate: 2, date: timestamp, timestamp: timestamp}] - , profile: {dia: 3, current_basal: 1, bolussnooze_dia_divisor: 2} - }; - - var hourLaterInputs = inputs; - hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.equal(0); - }); - - it('should show 0 IOB with Temp Basals if basal is percentage based', function() { - - var now = Date.now() - , timestamp = new Date(now).toISOString() - , timestampEarly = new Date(now - (60 * 60 * 1000)).toISOString() - , inputs = { - clock: timestamp - , history: [{_type: 'TempBasal', temp: 'percent', rate: 2, date: timestamp, timestamp: timestamp}, - {_type: 'TempBasalDuration','duration (min)': 30, date: timestamp}] - , profile: {dia: 3,current_basal: 1, bolussnooze_dia_divisor: 2} - }; - - - var hourLaterInputs = inputs; - hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - - hourLater.iob.should.equal(0); - }); - - - it('should calculate IOB using a 4 hour duration', function() { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 1, 'minutes': 0}]; - - var now = Date.now() - , timestamp = new Date(now).toISOString() - , inputs = { - clock: timestamp - , history: [{ - _type: 'Bolus' - , amount: 1 - , timestamp: timestamp - }] - , profile: { - dia: 4 - , bolussnooze_dia_divisor: 2 - , basalprofile: basalprofile - , current_basal: 1 - } - - }; - - var rightAfterBolus = require('../lib/iob')(inputs)[0]; - //console.log(rightAfterBolus); - rightAfterBolus.iob.should.equal(1); - rightAfterBolus.bolussnooze.should.equal(1); - - var hourLaterInputs = inputs; - hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); - var hourLater = require('../lib/iob')(hourLaterInputs)[0]; - hourLater.iob.should.be.lessThan(1); - hourLater.bolussnooze.should.be.lessThan(.5); - hourLater.iob.should.be.greaterThan(0); - - var after3hInputs = inputs; - after3hInputs.clock = new Date(now + (3 * 60 * 60 * 1000)).toISOString(); - var after3h = require('../lib/iob')(after3hInputs)[0]; - after3h.iob.should.be.greaterThan(0); - - var after4hInputs = inputs; - after4hInputs.clock = new Date(now + (4 * 60 * 60 * 1000)).toISOString(); - var after4h = require('../lib/iob')(after4hInputs)[0]; - after4h.iob.should.equal(0); - - }); +describe('IOB', function() { + + it('should calculate IOB', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 2, + timestamp: timestamp + }], + profile: { + dia: 3, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1 + } + + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(2); + rightAfterBolus.bolussnooze.should.equal(2); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(1.45); + hourLater.bolussnooze.should.be.lessThan(.5); + hourLater.iob.should.be.greaterThan(0); + hourLater.activity.should.be.greaterThan(0.01); + hourLater.activity.should.be.lessThan(0.02); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (3 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + it('should calculate IOB with Ultra-fast curve', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 2, + timestamp: timestamp + }], + profile: { + dia: 5, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1, + curve: 'ultra-rapid' + } + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + + rightAfterBolus.iob.should.equal(2); + rightAfterBolus.bolussnooze.should.equal(2); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(1.6); + hourLater.iob.should.be.greaterThan(1.3); + + hourLater.bolussnooze.should.be.lessThan(1.7); + hourLater.iob.should.be.greaterThan(0); + hourLater.activity.should.be.greaterThan(0.006); + hourLater.activity.should.be.lessThan(0.015); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (5 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + + it('should calculate IOB with Ultra-fast peak setting of 55', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 5, + insulinPeakTime: 55, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1, + curve: 'ultra-rapid' + } + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(1); + rightAfterBolus.bolussnooze.should.equal(1); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(0.75); + hourLater.bolussnooze.should.be.lessThan(0.75); + hourLater.iob.should.be.greaterThan(0); + hourLater.activity.should.be.greaterThan(0.0065); + hourLater.activity.should.be.lessThan(0.008); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (5 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + it('should calculate IOB with Ultra-fast curve peak setting of 65', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 5, + insulinPeakTime: 65, + useCustomPeakTime: true, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1, + curve: 'ultra-rapid' + } + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(1); + rightAfterBolus.bolussnooze.should.equal(1); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(0.77); + hourLater.bolussnooze.should.be.lessThan(0.36); + hourLater.iob.should.be.greaterThan(0.72); + hourLater.bolussnooze.should.be.greaterThan(0.354); + + hourLater.activity.should.be.greaterThan(0.0055); + hourLater.activity.should.be.lessThan(0.007); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (5 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + it('should calculate IOB with Ultra-rapid curve peak setting of 75', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 5, + insulinPeakTime: 75, + useCustomPeakTime: true, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1, + curve: 'ultra-rapid' + } + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(1); + rightAfterBolus.bolussnooze.should.equal(1); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(0.81); + hourLater.bolussnooze.should.be.lessThan(0.5); + hourLater.iob.should.be.greaterThan(0.76); + hourLater.bolussnooze.should.be.greaterThan(0.40); + + hourLater.iob.should.be.greaterThan(0); + hourLater.activity.should.be.greaterThan(0.0047); + hourLater.activity.should.be.lessThan(0.007); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (5 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + it('should calculate IOB with Ultra-rapid curve peak setting of 44 and DIA = 6', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 6, + insulinPeakTime: 44, + useCustomPeakTime: true, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1, + curve: 'ultra-rapid' + } + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(1); + rightAfterBolus.bolussnooze.should.equal(1); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(0.59); + hourLater.bolussnooze.should.be.lessThan(0.23); + + hourLater.iob.should.be.greaterThan(0.57); + hourLater.bolussnooze.should.be.greaterThan(0.21); + + hourLater.activity.should.be.greaterThan(0.007); + hourLater.activity.should.be.lessThan(0.0085); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (6 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + + + it('should calculate IOB with Rapid-acting', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 5, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1, + curve: 'rapid-acting' + } + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(1); + rightAfterBolus.bolussnooze.should.equal(1); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(0.8); + hourLater.bolussnooze.should.be.lessThan(.8); + hourLater.iob.should.be.greaterThan(0); + + var afterDIAInputs = inputs; + afterDIAInputs.clock = new Date(now + (5 * 60 * 60 * 1000)).toISOString(); + var afterDIA = require('../lib/iob')(afterDIAInputs)[0]; + + afterDIA.iob.should.equal(0); + afterDIA.bolussnooze.should.equal(0); + }); + + + it('should snooze fast if bolussnooze_dia_divisor is high', function() { + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 3, + bolussnooze_dia_divisor: 10 + } + }; + + var snoozeInputs = inputs; + snoozeInputs.clock = new Date(now + (20 * 60 * 1000)).toISOString(); + var snooze = require('../lib/iob')(snoozeInputs)[0]; + snooze.bolussnooze.should.equal(0); + }); + + it('should calculate IOB with Temp Basals', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + var now = Date.now(), + timestamp = new Date(now).toISOString(), + timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString(), + timestamp60mAgo = new Date(now - (60 * 60 * 1000)).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp60mAgo + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp60mAgo, + timestamp: timestamp60mAgo + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp30mAgo, + timestamp: timestamp30mAgo + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp + }], + profile: { + dia: 3, + current_basal: 1, + max_daily_basal: 1, + bolussnooze_dia_divisor: 2, + 'basalprofile': basalprofile + } + }; + + var iobInputs = inputs; + iobInputs.clock = timestamp + var iobNow = require('../lib/iob')(iobInputs)[0]; + + //console.log(iobNow); + iobNow.iob.should.be.lessThan(1); + iobNow.iob.should.be.greaterThan(0.5); + }); + + it('should calculate IOB with Temp Basals and a basal profile', function() { + + var startingPoint = moment('2016-06-13 01:00:00.000'); + var timestamp = startingPoint.format(); + var timestampEarly = startingPoint.subtract(30, 'minutes').format(); + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 2, + 'minutes': 0 + }, + { + 'i': 1, + 'start': '01:00:00', + 'rate': 1, + 'minutes': 60 + } + ]; + + var inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestampEarly + }, { + _type: 'TempBasal', + rate: 2, + date: timestampEarly, + timestamp: timestampEarly + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp, + timestamp: timestamp + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp + }], + profile: { + dia: 3, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = moment('2016-06-13 01:30:00.000'); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(0.5); + hourLater.iob.should.be.greaterThan(0.4); + }); + + it('should calculate IOB with Temp Basals that overlap midnight and a basal profile', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 2, + 'minutes': 0 + }, + { + 'i': 1, + 'start': '00:15:00', + 'rate': 1, + 'minutes': 15 + }, + { + 'i': 2, + 'start': '00:45:00', + 'rate': 0.5, + 'minutes': 45 + } + ]; + + var startingPoint = moment('2016-06-13 00:15:00.000'); + var timestamp = startingPoint.format(); + var timestampEarly = startingPoint.subtract(30, 'minutes').format(), + inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestampEarly + }, { + _type: 'TempBasal', + rate: 2, + date: timestampEarly, + timestamp: timestampEarly + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp, + timestamp: timestamp + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp + }], + profile: { + dia: 3, + current_basal: 0.1, + max_daily_basal: 1, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = moment('2016-06-13 00:45:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.be.lessThan(0.8); + hourLater.iob.should.be.greaterThan(0.7); + }); + + it('should calculate IOB with Temp Basals that overlap each other', function() { + + var nowDate = new Date(); + var now = Date.now(); + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var startingPoint = moment('2016-06-13 00:30:00.000'); + var timestampEarly = moment('2016-06-13 00:30:00.000').subtract(30, 'minutes'); + var timestampEarly2 = moment('2016-06-13 00:30:00.000').subtract(29, 'minutes'); + var timestampEarly3 = moment('2016-06-13 00:30:00.000').subtract(28, 'minutes'); + + var timestamp = startingPoint; + var inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestampEarly.unix() + }, { + _type: 'TempBasal', + rate: 2, + date: timestampEarly.unix(), + timestamp: timestampEarly.format() + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestampEarly2.unix() + }, { + _type: 'TempBasal', + rate: 2, + date: timestampEarly2.unix(), + timestamp: timestampEarly2.format() + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestampEarly3.unix() + }, { + _type: 'TempBasal', + rate: 2, + date: timestampEarly3.unix(), + timestamp: timestampEarly3.format() + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp.unix(), + timestamp: timestamp.format() + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp.unix() + }], + profile: { + dia: 3, + current_basal: 0.1, + max_daily_basal: 1, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = moment('2016-06-13 00:30:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.be.lessThan(0.5); + hourLater.iob.should.be.greaterThan(0.45); + }); + it('should calculate IOB with Temp Basals that overlap midnight and a basal profile, part deux', function() { + + var nowDate = new Date(); + var now = Date.now(); + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 2, + 'minutes': 0 + }, + { + 'i': 1, + 'start': '00:15:00', + 'rate': 0.1, + 'minutes': 15 + }, + { + 'i': 1, + 'start': '00:30:00', + 'rate': 2, + 'minutes': 30 + }, + { + 'i': 1, + 'start': '00:45:00', + 'rate': 0.1, + 'minutes': 45 + } + ]; + + var startingPoint = moment('2016-06-13 23:45:00.000'); + var timestamp = startingPoint.format(); + var timestampEarly = startingPoint.subtract(30, 'minutes').format(), + inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 60, + date: timestamp + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp, + timestamp: timestamp + }], + profile: { + dia: 3, + current_basal: 0.1, + max_daily_basal: 1, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = moment('2016-06-14 00:45:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.be.lessThan(1); + hourLater.iob.should.be.greaterThan(0.8); + }); + + + it('should not report negative IOB with Temp Basals and a basal profile with drastic changes', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 0.1, + 'minutes': 0 + }, + { + 'i': 1, + 'start': '00:30:00', + 'rate': 2, + 'minutes': 30 + } + ]; + + var startingPoint = new Date('2016-06-13 00:00:00.000'); + var startingPoint2 = new Date('2016-06-13 00:30:00.000'); + var endPoint = new Date('2016-06-13 01:00:00.000'); + + var inputs = { + clock: endPoint, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: startingPoint + }, { + _type: 'TempBasal', + rate: 0.1, + date: startingPoint, + timestamp: startingPoint + }, { + _type: 'TempBasal', + rate: 2, + date: startingPoint2, + timestamp: startingPoint2 + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: startingPoint2 + }], + profile: { + dia: 3, + current_basal: 2, + max_daily_basal: 2, + bolussnooze_dia_divisor: 2, + 'basalprofile': basalprofile + } + }; + + var hourLaterInputs = inputs; + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.equal(0); + }); + + it('should calculate IOB with Temp Basal events that overlap', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString(), + timestamp31mAgo = new Date(now - (31 * 60 * 1000)).toISOString(), + inputs = { + clock: timestamp30mAgo, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp31mAgo + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp31mAgo, + timestamp: timestamp31mAgo + }, { + _type: 'TempBasal', + rate: 2, + date: timestamp30mAgo, + timestamp: timestamp30mAgo + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp30mAgo + }], + profile: { + dia: 3, + current_basal: 1, + max_daily_basal: 1, + 'basalprofile': basalprofile + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.be.lessThan(1); + hourLater.iob.should.be.greaterThan(0); + + }); + + it('should calculate IOB with Temp Basals that are lower than base rate', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 2, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + timestampEarly = new Date(now - (30 * 60 * 1000)).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestampEarly + }, { + _type: 'TempBasal', + rate: 1, + date: timestampEarly, + timestamp: timestampEarly + }, { + _type: 'TempBasal', + rate: 1, + date: timestamp, + timestamp: timestamp + }, { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp + }], + profile: { + dia: 3, + current_basal: 2, + max_daily_basal: 2, + bolussnooze_dia_divisor: 2, + 'basalprofile': basalprofile + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.be.lessThan(0); + hourLater.iob.should.be.greaterThan(-1); + + }); + + it('should show 0 IOB with Temp Basals if duration is not found', function() { + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + timestampEarly = new Date(now - (60 * 60 * 1000)).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasal', + rate: 2, + date: timestamp, + timestamp: timestamp + }], + profile: { + dia: 3, + current_basal: 1, + max_daily_basal: 1, + bolussnooze_dia_divisor: 2 + } + }; + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.equal(0); + }); + + it('should show 0 IOB with Temp Basals if basal is percentage based', function() { + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + timestampEarly = new Date(now - (60 * 60 * 1000)).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'TempBasal', + temp: 'percent', + rate: 2, + date: timestamp, + timestamp: timestamp + }, + { + _type: 'TempBasalDuration', + 'duration (min)': 30, + date: timestamp + } + ], + profile: { + dia: 3, + current_basal: 1, + max_daily_basal: 1, + bolussnooze_dia_divisor: 2 + } + }; + + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + + hourLater.iob.should.equal(0); + }); + + + it('should calculate IOB using a 4 hour duration', function() { + + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 1, + 'minutes': 0 + }]; + + var now = Date.now(), + timestamp = new Date(now).toISOString(), + inputs = { + clock: timestamp, + history: [{ + _type: 'Bolus', + amount: 1, + timestamp: timestamp + }], + profile: { + dia: 4, + bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + current_basal: 1, + max_daily_basal: 1 + } + + }; + + var rightAfterBolus = require('../lib/iob')(inputs)[0]; + rightAfterBolus.iob.should.equal(1); + rightAfterBolus.bolussnooze.should.equal(1); + + var hourLaterInputs = inputs; + hourLaterInputs.clock = new Date(now + (60 * 60 * 1000)).toISOString(); + var hourLater = require('../lib/iob')(hourLaterInputs)[0]; + hourLater.iob.should.be.lessThan(1); + hourLater.bolussnooze.should.be.lessThan(.5); + hourLater.iob.should.be.greaterThan(0); + + var after3hInputs = inputs; + after3hInputs.clock = new Date(now + (3 * 60 * 60 * 1000)).toISOString(); + var after3h = require('../lib/iob')(after3hInputs)[0]; + after3h.iob.should.be.greaterThan(0); + + var after4hInputs = inputs; + after4hInputs.clock = new Date(now + (4 * 60 * 60 * 1000)).toISOString(); + var after4h = require('../lib/iob')(after4hInputs)[0]; + after4h.iob.should.equal(0); + + }); });