diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fcbc57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +bower_components/ +node_modules/ + + +bundle/bundle.out.js + +.idea/ +*.iml +my.env + +*.env +static/bower_components/ +.*.sw? +.DS_Store + +.vagrant +/iisnode + +# istanbul output +coverage/ + +npm-debug.log diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6e0d73e --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +TESTS=tests/*.js + +all: test + +test: + ./node_modules/.bin/mocha ${TESTS} diff --git a/bin/bgpredict.js b/bin/bgpredict.js deleted file mode 100644 index 588bb32..0000000 --- a/bin/bgpredict.js +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env node - -/* - Predict Blood Glucose (BG) - - Copyright (c) 2015 OpenAPS Contributors - - Released under MIT license. See the accompanying LICENSE.txt file for - full terms and conditions - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -function isfLookup() { - var now = new Date(); - //isf_data.sensitivities.sort(function (a, b) { return a.offset > b.offset }); - var isfSchedule = isf_data.sensitivities[isf_data.sensitivities.length - 1] - - for (var i = 0; i < isf_data.sensitivities.length - 1; i++) { - if ((now >= getTime(isf_data.sensitivities[i].offset)) && (now < getTime(isf_data.sensitivities[i + 1].offset))) { - isfSchedule = isf_data.sensitivities[i]; - break; - } - } - isf = isfSchedule.sensitivity; -} - - -if (!module.parent) { - - var glucose_input = process.argv.slice(2, 3).pop() - var iob_input = process.argv.slice(3, 4).pop() - var isf_input = process.argv.slice(4, 5).pop() - - if (!glucose_input || !iob_input || !isf_input) { - console.log('usage: ', process.argv.slice(0, 2), ' '); - process.exit(1); - } - - var cwd = process.cwd() - var glucose_data = require(cwd + '/' + glucose_input); - var bgnow = glucose_data[0].glucose; - var delta = bgnow - glucose_data[1].glucose; - var tick; - if (delta < 0) { tick = delta; } else { tick = "+" + delta; } - var iob_data = require(cwd + '/' + iob_input); - iob = iob_data.iob.toFixed(2); - var isf_data = require(cwd + '/' + isf_input); - var isf; - isfLookup(); - var eventualBG = Math.round( bgnow - ( iob * isf ) ); - - var prediction = { "bg" : bgnow, "iob" : iob, "eventualBG" : eventualBG } - - console.log(JSON.stringify(prediction)); -} diff --git a/bin/clockset.sh b/bin/clockset.sh index 84f6a56..1da17db 100755 --- a/bin/clockset.sh +++ b/bin/clockset.sh @@ -20,7 +20,7 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin die() { echo "$@" ; exit 1; } -ntp-wait -n 1 -v && die "NTP already synchronized." +ntp-wait -n 1 -v && die "NTP already synchronized." || ( sudo /etc/init.d/ntp restart && ntp-wait -n 1 -v && die "NTP re-synchronized." ) cd ~/openaps-dev ( cat clock.json; echo ) | sed 's/"//g' | sed "s/$/`date +%z`/" | while read line; do date -u -d $line +"%F %R:%S"; done > fake-hwclock.data diff --git a/bin/determine-basal.js b/bin/determine-basal.js deleted file mode 100644 index 7b877e3..0000000 --- a/bin/determine-basal.js +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env node - -/* - Determine Basal - - Released under MIT license. See the accompanying LICENSE.txt file for - full terms and conditions - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -function getLastGlucose(data) { - - var now = data[0]; - var last = data[1]; - var avg; - //TODO: calculate average using system_time instead of assuming 1 data point every 5m - if (typeof data[3] !== 'undefined' && data[3].glucose > 30) { - avg = ( now.glucose - data[3].glucose) / 3; - } else if (typeof data[2] !== 'undefined' && data[2].glucose > 30) { - avg = ( now.glucose - data[2].glucose) / 2; - } else if (typeof data[1] !== 'undefined' && data[1].glucose > 30) { - avg = now.glucose - data[1].glucose; - } else { avg = 0; } - var o = { - delta: now.glucose - last.glucose - , glucose: now.glucose - , avgdelta: avg - }; - - return o; - -} - -function setTempBasal(rate, duration) { - - maxSafeBasal = Math.min(profile_data.max_basal, 3 * profile_data.max_daily_basal, 4 * profile_data.current_basal); - - if (rate < 0) { rate = 0; } // if >30m @ 0 required, zero temp will be extended to 30m instead - else if (rate > maxSafeBasal) { rate = maxSafeBasal; } - - // rather than canceling temps, if Offline mode is set, always set the current basal as a 30m temp - // so we can see on the pump that openaps is working - if (duration == 0 && offline_input == 'Offline') { - rate = profile_data.current_basal; - duration = 30; - } - - requestedTemp.duration = duration; - requestedTemp.rate = Math.round((Math.round(rate / 0.05) * 0.05)*100)/100; -}; - - -if (!module.parent) { - var iob_input = process.argv.slice(2, 3).pop() - var temps_input = process.argv.slice(3, 4).pop() - var glucose_input = process.argv.slice(4, 5).pop() - var profile_input = process.argv.slice(5, 6).pop() - var offline_input = process.argv.slice(6, 7).pop() - - if (!iob_input || !temps_input || !glucose_input || !profile_input) { - console.error('usage: ', process.argv.slice(0, 2), ' [Offline]'); - process.exit(1); - } - - var cwd = process.cwd() - var glucose_data = require(cwd + '/' + glucose_input); - var temps_data = require(cwd + '/' + temps_input); - var iob_data = require(cwd + '/' + iob_input); - var profile_data = require(cwd + '/' + profile_input); - - if (typeof profile_data === 'undefined' || typeof profile_data.current_basal === 'undefined') { - console.error('Error: could not get current basal rate'); - process.exit(1); - } - - var max_iob = profile_data.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver - - // if target_bg is set, great. otherwise, if min and max are set, then set target to their average - var target_bg; - if (typeof profile_data.target_bg !== 'undefined') { - target_bg = profile_data.target_bg; - } else { - if (typeof profile_data.max_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') { - target_bg = (profile_data.min_bg + profile_data.max_bg) / 2; - } else { - console.error('Error: could not determine target_bg'); - process.exit(1); - } - } - - var glucose_status = getLastGlucose(glucose_data); - var bg = glucose_status.glucose; - var tick; - if (glucose_status.delta >= 0) { tick = "+" + glucose_status.delta; } - else { tick = glucose_status.delta; } - console.error("IOB: " + iob_data.iob.toFixed(2) + ", Bolus IOB: " + iob_data.bolusiob.toFixed(2)); - //calculate BG impact: the amount BG "should" be rising or falling based on insulin activity alone - var bgi = -iob_data.activity * profile_data.sens * 5; - console.error("Avg. Delta: " + glucose_status.avgdelta.toFixed(1) + ", BGI: " + bgi.toFixed(1)); - // project deviation over next 15 minutes - var deviation = Math.round( 15 / 5 * ( glucose_status.avgdelta - bgi ) ); - console.error("15m deviation: " + deviation.toFixed(0)); - // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity - var naive_eventualBG = Math.round( bg - (iob_data.iob * profile_data.sens) ); - // and adjust it for the deviation above - var eventualBG = naive_eventualBG + deviation; - // calculate what portion of that is due to bolusiob - var bolusContrib = iob_data.bolusiob * profile_data.sens; - // and add it back in to get snoozeBG, plus another 50% to avoid low-temping at mealtime - var naive_snoozeBG = Math.round( naive_eventualBG + 1.5 * bolusContrib ); - // adjust that for deviation like we did eventualBG - var snoozeBG = naive_snoozeBG + deviation; - console.error("BG: " + bg + tick + " -> " + eventualBG + "-" + snoozeBG + " (Unadjusted: " + naive_eventualBG + "-" + naive_snoozeBG + ")"); - if (typeof eventualBG === 'undefined') { console.error('Error: could not calculate eventualBG'); } - var requestedTemp = { - 'temp': 'absolute' - , 'bg': bg - , 'tick': tick - , 'eventualBG': eventualBG - , 'snoozeBG': snoozeBG - }; - - - - //if old reading from Dexcom do nothing - - var systemTime = new Date(); - var bgTime; - if (glucose_data[0].display_time) { - bgTime = new Date(glucose_data[0].display_time.replace('T', ' ')); - } else if (glucose_data[0].dateString) { - bgTime = new Date(glucose_data[0].dateString); - } else { console.error("Could not determine last BG time"); } - var minAgo = (systemTime - bgTime) / 60 / 1000 - var threshold = profile_data.min_bg - 30; - var reason=""; - - if (minAgo < 10 && minAgo > -5) { // Dexcom data is recent, but not far in the future - - if (bg > 30) { //Dexcom is in ??? mode or calibrating, do nothing. Asked @benwest for raw data in iter_glucose - - if (bg < threshold) { // low glucose suspend mode: BG is < ~80 - reason = "BG " + bg + "<" + threshold; - console.error(reason); - if (glucose_status.delta > 0) { // if BG is rising - if (temps_data.rate > profile_data.current_basal) { // if a high-temp is running - setTempBasal(0, 0); // cancel high temp - } else if (temps_data.duration && eventualBG > profile_data.max_bg) { // if low-temped and predicted to go high from negative IOB - setTempBasal(0, 0); // cancel low temp - } else { - reason = bg + "<" + threshold + "; no high-temp to cancel"; - console.error(reason); - } - } - else { // BG is not yet rising - setTempBasal(0, 30); - } - - } else { - - // if BG is rising but eventual BG is below min, or BG is falling but eventual BG is above min - if ((glucose_status.delta > 0 && eventualBG < profile_data.min_bg) || (glucose_status.delta < 0 && eventualBG >= profile_data.min_bg)) { - if (temps_data.duration > 0) { // if there is currently any temp basal running - // if it's a low-temp and eventualBG < profile_data.max_bg, let it run a bit longer - if (temps_data.rate <= profile_data.current_basal && eventualBG < profile_data.max_bg) { - reason = "BG" + tick + " but eventualBG " + eventualBG + "<" + profile_data.max_bg; - console.error(reason); - } else { - reason = tick + " and eventualBG " + eventualBG; - setTempBasal(0, 0); // cancel temp - } - } else { - reason = tick + "; no temp to cancel"; - console.error(reason); - } - - } else if (eventualBG < profile_data.min_bg) { // if eventual BG is below target: - // if this is just due to boluses, we can snooze until the bolus IOB decays (at double speed) - if (snoozeBG > profile_data.min_bg) { // if adding back in the bolus contribution BG would be above min - // if BG is falling and high-temped, or rising and low-temped, cancel - if (glucose_status.delta < 0 && temps_data.rate > profile_data.current_basal) { - reason = tick + " and temp " + temps_data.rate + " > basal " + profile_data.current_basal; - setTempBasal(0, 0); // cancel temp - } else if (glucose_status.delta > 0 && temps_data.rate < profile_data.current_basal) { - reason = tick + " and temp " + temps_data.rate + " < basal " + profile_data.current_basal; - setTempBasal(0, 0); // cancel temp - } else { - reason = "bolus snooze: eventual BG range " + eventualBG + "-" + snoozeBG; - console.error(reason); - } - } else { - // calculate 30m low-temp required to get projected BG up to target - // negative insulin required to get up to min: - //var insulinReq = Math.max(0, (target_bg - eventualBG) / profile_data.sens); - // use snoozeBG instead of eventualBG to more gradually ramp in any counteraction of the user's boluses - var insulinReq = Math.min(0, (snoozeBG - target_bg) / profile_data.sens); - // rate required to deliver insulinReq less insulin over 30m: - var rate = profile_data.current_basal + (2 * insulinReq); - rate = Math.round( rate * 1000 ) / 1000; - // if required temp < existing temp basal - if (typeof temps_data.rate !== 'undefined' && (temps_data.duration > 0 && rate > temps_data.rate - 0.1)) { - reason = "temp " + temps_data.rate + " <~ req " + rate + "U/hr"; - console.error(reason); - } else { - reason = "Eventual BG " + eventualBG + "<" + profile_data.min_bg; - //console.error(reason); - setTempBasal(rate, 30); - } - } - - } else if (eventualBG > profile_data.max_bg) { // if eventual BG is above target: - // if iob is over max, just cancel any temps - var basal_iob = Math.round(( iob_data.iob - iob_data.bolusiob )*1000)/1000; - if (basal_iob > max_iob) { - reason = "basal_iob " + basal_iob + " > max_iob " + max_iob; - setTempBasal(0, 0); - } else { - // calculate 30m high-temp required to get projected BG down to target - // additional insulin required to get down to max bg: - var insulinReq = (eventualBG - target_bg) / profile_data.sens; - // if that would put us over max_iob, then reduce accordingly - if (insulinReq > max_iob-basal_iob) { - reason = "max_iob " + max_iob + ", "; - insulinReq = max_iob-basal_iob; - } - - // rate required to deliver insulinReq more insulin over 30m: - var rate = profile_data.current_basal + (2 * insulinReq); - rate = Math.round( rate * 1000 ) / 1000; - - maxSafeBasal = Math.min(profile_data.max_basal, 3 * profile_data.max_daily_basal, 4 * profile_data.current_basal); - if (rate > maxSafeBasal) { - rate = maxSafeBasal; - //console.error(maxSafeBasal); - } - var insulinScheduled = temps_data.duration * (temps_data.rate - profile_data.current_basal) / 60; - if (insulinScheduled > insulinReq + 0.3) { // if current temp would deliver >0.3U more than the required insulin, lower the rate - reason = temps_data.duration + "@" + temps_data.rate + " > req " + insulinReq + "U"; - setTempBasal(rate, 30); - } - else if (typeof temps_data.rate == 'undefined' || temps_data.rate == 0) { // no temp is set - reason += "no temp, setting " + rate + "U/hr"; - setTempBasal(rate, 30); - } - else if (temps_data.duration > 0 && rate < temps_data.rate + 0.1) { // if required temp <~ existing temp basal - reason += "temp " + temps_data.rate + " >~ req " + rate + "U/hr"; - console.error(reason); - } else { // required temp > existing temp basal - reason += "temp " + temps_data.rate + "<" + rate + "U/hr"; - setTempBasal(rate, 30); - } - } - - } else { - reason = eventualBG + " is in range. No temp required."; - if (temps_data.duration > 0) { // if there is currently any temp basal running - setTempBasal(0, 0); // cancel temp - } else { - console.error(reason); - } - } - } - - if (offline_input == 'Offline') { - // if no temp is running or required, set the current basal as a temp, so you can see on the pump that the loop is working - if ((!temps_data.duration || (temps_data.rate == profile_data.current_basal)) && !requestedTemp.duration) { - reason = reason + "; setting current basal of " + profile_data.current_basal + " as temp"; - setTempBasal(profile_data.current_basal, 30); - } - } - } else { - reason = "CGM is calibrating or in ??? state"; - console.error(reason); - } - } else { - reason = "BG data is too old, or clock set incorrectly"; - console.error(reason); - } - - -requestedTemp.reason = reason; -console.log(JSON.stringify(requestedTemp)); -} diff --git a/bin/getprofile.js b/bin/getprofile.js deleted file mode 100644 index 8c9d564..0000000 --- a/bin/getprofile.js +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env node - -/* - Get Basal Information - - Released under MIT license. See the accompanying LICENSE.txt file for - full terms and conditions - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -*/ - -function getTime(minutes) { - var baseTime = new Date(); - baseTime.setHours('00'); - baseTime.setMinutes('00'); - baseTime.setSeconds('00'); - - return baseTime.getTime() + minutes * 60 * 1000; - -} - -/* Return basal rate(U / hr) at the provided timeOfDay */ - -function basalLookup() { - var now = new Date(); - var basalRate = basalprofile_data[basalprofile_data.length-1].rate - - for (var i = 0; i < basalprofile_data.length - 1; i++) { - if ((now >= getTime(basalprofile_data[i].minutes)) && (now < getTime(basalprofile_data[i + 1].minutes))) { - basalRate = basalprofile_data[i].rate; - break; - } - } - profile.current_basal= Math.round(basalRate*1000)/1000; -} - -function bgTargetsLookup(){ - var now = new Date(); - - //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset }); - var bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1] - - for (var i = 0; i < bgtargets_data.targets.length - 1; i++) { - if ((now >= getTime(bgtargets_data.targets[i].offset)) && (now < getTime(bgtargets_data.targets[i + 1].offset))) { - bgTargets = bgtargets_data.targets[i]; - break; - } - } - profile.max_bg = bgTargets.high; - profile.min_bg = bgTargets.low; -} - -function carbRatioLookup() { - var now = new Date(); - //carbratio_data.schedule.sort(function (a, b) { return a.offset > b.offset }); - var carbRatio = carbratio_data.schedule[carbratio_data.schedule.length - 1] - - for (var i = 0; i < carbratio_data.schedule.length - 1; i++) { - if ((now >= getTime(carbratio_data.schedule[i].offset)) && (now < getTime(carbratio_data.schedule[i + 1].offset))) { - carbRatio = carbratio_data.schedule[i]; - break; - } - } - profile.carbratio = carbRatio.ratio; -} - -function isfLookup() { - var now = new Date(); - //isf_data.sensitivities.sort(function (a, b) { return a.offset > b.offset }); - var isfSchedule = isf_data.sensitivities[isf_data.sensitivities.length - 1] - - for (var i = 0; i < isf_data.sensitivities.length - 1; i++) { - if ((now >= getTime(isf_data.sensitivities[i].offset)) && (now < getTime(isf_data.sensitivities[i + 1].offset))) { - isfSchedule = isf_data.sensitivities[i]; - break; - } - } - profile.sens = isfSchedule.sensitivity; -} - -function maxDailyBasal(){ - basalprofile_data.sort(function (a, b) { if (a.rate < b.rate) { return 1 } if (a.rate > b.rate) { return -1; } return 0; }); - profile.max_daily_basal = Math.round( basalprofile_data[0].rate *1000)/1000; -} - -/*Return maximum daily basal rate(U / hr) from profile.basals */ - -function maxBasalLookup() { - - profile.max_basal =pumpsettings_data.maxBasal; -} - -if (!module.parent) { - - var pumpsettings_input = process.argv.slice(2, 3).pop() - var bgtargets_input = process.argv.slice(3, 4).pop() - var isf_input = process.argv.slice(4, 5).pop() - var basalprofile_input = process.argv.slice(5, 6).pop() - var carbratio_input = process.argv.slice(6, 7).pop() - var maxiob_input = process.argv.slice(7, 8).pop() - - if (!pumpsettings_input || !bgtargets_input || !isf_input || !basalprofile_input || !carbratio_input) { - console.log('usage: ', process.argv.slice(0, 2), ' []'); - process.exit(1); - } - - var cwd = process.cwd() - var pumpsettings_data = require(cwd + '/' + pumpsettings_input); - var bgtargets_data = require(cwd + '/' + bgtargets_input); - var isf_data = require(cwd + '/' + isf_input); - var basalprofile_data = require(cwd + '/' + basalprofile_input); - var carbratio_data = require(cwd + '/' + carbratio_input);; - - var profile = { - carbs_hr: 28 // TODO: verify this is completely unused and consider removing it if so - , max_iob: 0 // if max_iob.json is not profided, never give more insulin than the pump would have - , dia: pumpsettings_data.insulin_action_curve - , type: "current" - }; - - basalLookup(); - maxDailyBasal(); - maxBasalLookup() - bgTargetsLookup(); - carbRatioLookup(); - isfLookup(); - if (typeof maxiob_input != 'undefined') { - var maxiob_data = require(cwd + '/' + maxiob_input); - profile.max_iob = maxiob_data.max_iob; - } - - console.log(JSON.stringify(profile)); -} diff --git a/bin/iob.js b/bin/iob.js deleted file mode 100644 index 91c51e2..0000000 --- a/bin/iob.js +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env node - -/* - Insulin On Board (IOB) calculations. - - IOB is also known as "Bolus on Board", "Active Insulin", or "Insulin Remaining" - - Released under MIT license. See the accompanying LICENSE.txt file for - full terms and conditions - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -*/ - -function iobCalc(treatment, time, dia) { - var diaratio = dia / 3; - var peak = 75 * diaratio; - //var sens = profile_data.sens; - if (typeof time === 'undefined') { - var time = new Date(); - } - - if (treatment.insulin) { - var bolusTime=new Date(treatment.date); - var minAgo=(time-bolusTime)/1000/60; - - if (minAgo < 0) { - var iobContrib=0; - var activityContrib=0; - } - if (minAgo < peak) { - var x = (minAgo/5 + 1) * diaratio; - var iobContrib=treatment.insulin*(1-0.001852*x*x+0.001852*x); - //var activityContrib=sens*treatment.insulin*(2/dia/60/peak)*minAgo; - var activityContrib=treatment.insulin*(2/dia/60/peak)*minAgo; - - } - else if (minAgo < 180) { - var x = (minAgo-peak)/5 * diaratio; - var iobContrib=treatment.insulin*(0.001323*x*x - .054233*x + .55556); - //var activityContrib=sens*treatment.insulin*(2/dia/60-(minAgo-peak)*2/dia/60/(60*dia-peak)); - var activityContrib=treatment.insulin*(2/dia/60-(minAgo-peak)*2/dia/60/(60*dia-peak)); - } - else { - var iobContrib=0; - var activityContrib=0; - } - return { - iobContrib: iobContrib, - activityContrib: activityContrib - }; - } - else { - return ''; - } -} -function iobTotal(treatments, time) { - var iob = 0; - var bolusiob = 0; - var activity = 0; - if (!treatments) return {}; - //if (typeof time === 'undefined') { - //var time = new Date(); - //} - - treatments.forEach(function(treatment) { - if(treatment.date < time.getTime( )) { - var dia = profile_data.dia; - var tIOB = iobCalc(treatment, time, dia); - if (tIOB && tIOB.iobContrib) iob += tIOB.iobContrib; - if (tIOB && tIOB.activityContrib) activity += tIOB.activityContrib; - // keep track of bolus IOB separately for snoozes, but decay it three times as fast - if (treatment.insulin >= 0.2 && treatment.started_at) { - var bIOB = iobCalc(treatment, time, dia/3) - //console.log(treatment); - //console.log(bIOB); - if (bIOB && bIOB.iobContrib) bolusiob += bIOB.iobContrib; - } - } - }); - - return { - iob: iob, - activity: activity, - bolusiob: bolusiob - }; -} - -function calcTempTreatments() { - var tempHistory = []; - var tempBoluses = []; - var now = new Date(); - var timeZone = now.toString().match(/([-\+][0-9]+)\s/)[1] - for (var i=0; i < pumpHistory.length; i++) { - var current = pumpHistory[i]; - //if(pumpHistory[i].date < time) { - if (pumpHistory[i]._type == "Bolus") { - //console.log(pumpHistory[i]); - var temp = {}; - temp.timestamp = current.timestamp; - //temp.started_at = new Date(current.date); - temp.started_at = new Date(current.timestamp + timeZone); - //temp.date = current.date - temp.date = temp.started_at.getTime(); - temp.insulin = current.amount - tempBoluses.push(temp); - } else if (pumpHistory[i]._type == "TempBasal") { - if (current.temp == 'percent') { - continue; - } - var rate = pumpHistory[i].rate; - var date = pumpHistory[i].date; - if (i>0 && pumpHistory[i-1].date == date && pumpHistory[i-1]._type == "TempBasalDuration") { - var duration = pumpHistory[i-1]['duration (min)']; - } else if (i+1 tempHistory[i+1].date) { - tempHistory[i].duration = (tempHistory[i+1].date - tempHistory[i].date)/60/1000; - } - } - var tempBolusSize; - var now = new Date(); - var timeZone = now.toString().match(/([-\+][0-9]+)\s/)[1] - for (var i=0; i < tempHistory.length; i++) { - if (tempHistory[i].duration > 0) { - var netBasalRate = tempHistory[i].rate-profile_data.current_basal; - if (netBasalRate < 0) { tempBolusSize = -0.05; } - else { tempBolusSize = 0.05; } - var netBasalAmount = Math.round(netBasalRate*tempHistory[i].duration*10/6)/100 - var tempBolusCount = Math.round(netBasalAmount / tempBolusSize); - var tempBolusSpacing = tempHistory[i].duration / tempBolusCount; - for (var j=0; j < tempBolusCount; j++) { - var tempBolus = {}; - tempBolus.insulin = tempBolusSize; - tempBolus.date = tempHistory[i].date + j * tempBolusSpacing*60*1000; - tempBolus.created_at = new Date(tempBolus.date); - tempBoluses.push(tempBolus); - } - } - } - return [ ].concat(tempBoluses).concat(tempHistory); - return { - tempBoluses: tempBoluses, - tempHistory: tempHistory - }; - -} - -if (!module.parent) { - var iob_input = process.argv.slice(2, 3).pop() - var profile_input = process.argv.slice(3, 4).pop() - var clock_input = process.argv.slice(4, 5).pop() - if (!iob_input || !profile_input) { - console.log('usage: ', process.argv.slice(0, 2), ' '); - process.exit(1); - } - var cwd = process.cwd() - var all_data = require(cwd + '/' + iob_input); - var profile_data = require(cwd + '/' + profile_input); - var clock_data = require(cwd + '/' + clock_input); - var pumpHistory = all_data; - pumpHistory.reverse( ); - - - var all_treatments = calcTempTreatments( ); - //console.log(all_treatments); - var treatments = all_treatments; // .tempBoluses.concat(all_treatments.tempHistory); - treatments.sort(function (a, b) { return a.date > b.date }); - //var lastTimestamp = new Date(treatments[treatments.length -1].date + 1000 * 60); - //console.log(clock_data); - var now = new Date(); - var timeZone = now.toString().match(/([-\+][0-9]+)\s/)[1] - var clock_iso = clock_data + timeZone; - var clock = new Date(clock_iso); - //console.log(clock); - var iob = iobTotal(treatments, clock); - //var iobs = iobTotal(treatments, lastTimestamp); - // console.log(iobs); - console.log(JSON.stringify(iob)); -} - diff --git a/bin/loop.sh b/bin/loop.sh deleted file mode 100755 index 7036499..0000000 --- a/bin/loop.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -# Attempt to read from a Carelink reader, upload data, and calculate the new -# glucose value. -# -# Released under MIT license. See the accompanying LICENSE.txt file for -# full terms and conditions -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - -die() { - echo "$@" | tee -a /var/log/openaps/easy.log - exit 1 -} - -# remove any old stale lockfiles -find /tmp/openaps.lock -mmin +10 -exec rm {} \; 2>/dev/null > /dev/null - -# only one process can talk to the pump at a time -ls /tmp/openaps.lock >/dev/null 2>/dev/null && die "OpenAPS already running: exiting" && exit - -echo "No lockfile: continuing" -touch /tmp/openaps.lock - -# make sure decocare can talk to the Carelink USB stick -~/decocare/insert.sh 2>/dev/null >/dev/null -python -m decocare.stick $(python -m decocare.scan) >/dev/null && echo "decocare.scan OK" || sudo ~/openaps-js/bin/fix-dead-carelink.sh | tee -a /var/log/openaps/easy.log - -# sometimes git gets stuck -find ~/openaps-dev/.git/index.lock -mmin +5 -exec rm {} \; 2>/dev/null > /dev/null -cd ~/openaps-dev && ( git status > /dev/null || ( mv ~/openaps-dev/.git /tmp/.git-`date +%s`; cd && openaps init openaps-dev && cd openaps-dev ) ) -# sometimes openaps.ini gets truncated -openaps report show > /dev/null || cp openaps.ini.bak openaps.ini - -function finish { - rm /tmp/openaps.lock -} -trap finish EXIT - -# define functions for everything we'll be doing - -# get glucose data, either from attached CGM or from Share -getglucose() { - echo "Querying CGM" - ( ( openaps report invoke glucose.json.new || openaps report invoke glucose.json.new ) && grep -v '"glucose": 5' glucose.json.new | grep glucose ) || share2-bridge file glucose.json.new - if diff -u glucose.json glucose.json.new; then - echo No new glucose data - else - grep glucose glucose.json.new | head -1 | awk '{print $2}' | while read line; do echo -n " $line "; done >> /var/log/openaps/easy.log \ - && rsync -tu glucose.json.new glucose.json \ - && git commit -m"glucose.json has glucose data: committing" glucose.json - fi -} -# get pump status (suspended, etc.) -getpumpstatus() { - echo "Checking pump status" - openaps status || echo -n "!" >> /var/log/openaps/easy.log - grep -q status status.json.new && ( rsync -tu status.json.new status.json && echo -n "." >> /var/log/openaps/easy.log ) || echo -n "!" >> /var/log/openaps/easy.log -} -# query pump, and update pump data files if successful -querypump() { - openaps pumpquery || openaps pumpquery || echo -n "!" >> /var/log/openaps/easy.log - findclocknew && grep T clock.json.new && ( rsync -tu clock.json.new clock.json && echo -n "." >> /var/log/openaps/easy.log ) || echo -n "!" >> /var/log/openaps/easy.log - grep -q temp currenttemp.json.new && ( rsync -tu currenttemp.json.new currenttemp.json && echo -n "." >> /var/log/openaps/easy.log ) || echo -n "!" >> /var/log/openaps/easy.log - grep -q timestamp pumphistory.json.new && ( rsync -tu pumphistory.json.new pumphistory.json && echo -n "." >> /var/log/openaps/easy.log ) || echo -n "!" >> /var/log/openaps/easy.log - upload -} -# try to upload pumphistory data -upload() { - #findpumphistory && ~/bin/openaps-mongo.sh & - ping -c 1 google.com > /dev/null && touch /tmp/openaps.online -} -# if we haven't uploaded successfully in 10m, use offline mode (if no temp running, set current basal as temp to show the loop is working) -suggest() { - openaps suggest || echo -n "!" >> /var/log/openaps/easy.log - grep -q "too old" requestedtemp.online.json || ( find /tmp/openaps.online -mmin -10 | egrep -q '.*' && rsync -tu requestedtemp.online.json requestedtemp.json || rsync -tu requestedtemp.offline.json requestedtemp.json ) -} -# get updated pump settings (basal schedules, targets, ISF, etc.) -getpumpsettings() { ~/openaps-js/bin/pumpsettings.sh; } - -# functions for making sure we have up-to-date data before proceeding -findclocknew() { find clock.json.new -mmin -10 | egrep -q '.*'; } -findglucose() { find glucose.json -mmin -10 | egrep -q '.*'; } -findpumphistory() { find pumphistory.json -mmin -10 | egrep -q '.*'; } -findrequestedtemp() { find requestedtemp.json -mmin -10 | egrep -q '.*'; } -# write out current status to pebble.json -pebble() { ~/openaps-js/bin/pebble.sh; } - - -# main event loop - -getglucose -head -15 glucose.json - -numprocs=$(fuser -n file $(python -m decocare.scan) 2>&1 | wc -l) -if [[ $numprocs -gt 0 ]] ; then - die "Carelink USB already in use or not available." -fi - -getpumpstatus -echo "Querying pump" && querypump - -upload - -# get glucose again in case the pump queries took awhile -getglucose - -# if we're offline, set the clock to the pump/CGM time -~/openaps-js/bin/clockset.sh - -# dump out a "what we're about to try to do" report -suggest && pebble - -tail clock.json -tail currenttemp.json - -# make sure we're not using an old suggestion -rm requestedtemp* -# if we can't run suggest, it might be because our pumpsettings are missing or screwed up" -suggest || ( getpumpsettings && suggest ) || die "Can't calculate IOB or basal" -pebble -tail profile.json -tail iob.json -tail requestedtemp.json - -# don't act on stale glucose data -findglucose && grep -q glucose glucose.json || die "No recent glucose data" -# execute/enact the requested temp -cat requestedtemp.json | json_pp | grep reason >> /var/log/openaps/easy.log -grep -q rate requestedtemp.json && ( openaps enact || openaps enact ) && tail enactedtemp.json && ( echo && cat enactedtemp.json | egrep -i "bg|rate|dur|re|tic|tim" | sort -r ) >> /var/log/openaps/easy.log && cat iob.json | json_pp | grep '"iob' >> /var/log/openaps/easy.log - -echo "Re-querying pump" -query pump - -# unlock in case upload is really slow -rm /tmp/openaps.lock -pebble -upload - -# if another instance didn't start while we were uploading, refresh pump settings -ls /tmp/openaps.lock >/dev/null 2>/dev/null && die "OpenAPS already running: exiting" && exit -touch /tmp/openaps.lock -getpumpsettings diff --git a/bin/loop.sh b/bin/loop.sh new file mode 120000 index 0000000..3efe45d --- /dev/null +++ b/bin/loop.sh @@ -0,0 +1 @@ +../../openaps-sh/loop.sh \ No newline at end of file diff --git a/bin/mm-format-ns-glucose.sh b/bin/mm-format-ns-glucose.sh new file mode 100755 index 0000000..5dba394 --- /dev/null +++ b/bin/mm-format-ns-glucose.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Author: Ben West @bewest +# Maintainer: Chris Oattes @cjo20 + +# Written for decocare v0.0.17. Will need updating the the decocare json format changes. +HISTORY=${1-glucosehistory.json} +OUTPUT=${2-/dev/fd/1} +#TZ=${3-$(date +%z)} + +cat $HISTORY | \ + json -e "this.medtronic = this._type;" | \ + json -e "this.dateString = this.date + '$(date +%z)'" | \ + json -e "this.date = new Date(this.dateString).getTime();" | \ + json -e "this.type = (this.name == 'GlucoseSensorData') ? 'sgv' : 'pumpdata'" | \ + json -e "this.device = 'openaps://medtronic/pump/cgm'" \ + > $OUTPUT + diff --git a/bin/mm-format-ns-pump-history.sh b/bin/mm-format-ns-pump-history.sh new file mode 100755 index 0000000..6556249 --- /dev/null +++ b/bin/mm-format-ns-pump-history.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Author: Ben West +# Maintainer: Scott Leibrand + +HISTORY=${1-pumphistory.json} +OUTPUT=${2-/dev/fd/1} +#TZ=${3-$(date +%z)} + +cat $HISTORY | \ + json -e "this.medtronic = this._type;" | \ + #json -e "this.dateString = this.timestamp + '$(TZ=TZ date +%z)'" | \ + json -e "this.dateString = this.timestamp + '$(date +%z)'" | \ + json -e "this.type = 'medtronic'" | \ + json -e "this.date = this.date ? this.date : new Date(Date.parse(this.dateString)).getTime( )" \ + > $OUTPUT + + diff --git a/bin/mm-stick.sh b/bin/mm-stick.sh new file mode 100755 index 0000000..0552985 --- /dev/null +++ b/bin/mm-stick.sh @@ -0,0 +1,119 @@ +#!/bin/bash -eu + +# Author: Ben West @bewest + +# Written for decocare v0.0.17. +OUTPUT=/dev/fd/1 +if [[ "${1-}" == "-f" ]] ; then +shift +OUTPUT=$1 +shift +fi +OPERATION=${1-help} +export $OPERATION + +function print_help ( ) { + cat < /dev/null + ;; + remove) + eval modprobe -r usbserial + ;; + insert) + #Bus 002 Device 011: ID 0a21:8001 Medtronic Physio Control Corp. + eval modprobe --first-time usbserial vendor=0x0a21 product=0x8001 + ;; + udev-info) + eval udevadm info --query=all $(python -m decocare.scan) + ;; + reset-usb) + if [[ $EUID != 0 ]] ; then + echo This must be run as root! + exit 1 + fi + + for xhci in /sys/bus/pci/drivers/?hci_hcd ; do + + if ! cd $xhci ; then + echo Weird error. Failed to change directory to $xhci + exit 1 + fi + + echo Resetting devices from $xhci... + + for i in ????:??:??.? ; do + echo -n "$i" > unbind + echo -n "$i" > bind + done + done + ;; + list-usb) + if [[ $EUID != 0 ]] ; then + echo This must be run as root! + exit 1 + fi + + for xhci in /sys/bus/pci/drivers/?hci_hcd ; do + + if ! cd $xhci ; then + echo Weird error. Failed to change directory to $xhci + exit 1 + fi + + echo Resetting devices from $xhci... + + for i in ????:??:??.? ; do + pwd + echo $i + ls $i + done + done + ;; + fail) + print_fail $* + exit 1 + ;; + *|help) + print_help + ;; + esac +) +shift +OPERATION=${1-} +done > $OUTPUT +exit $? diff --git a/bin/ns-upload-entries.sh b/bin/ns-upload-entries.sh new file mode 100755 index 0000000..0bb0f56 --- /dev/null +++ b/bin/ns-upload-entries.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Author: Ben West +# Maintainer: @cjo20, @scottleibrand + +ENTRIES=${1-entries.json} +NIGHTSCOUT_HOST=${NIGHTSCOUT_HOST-localhost:1337} +#TZ=${3-$(date +%z)} +OUTPUT=${2} + +export ENTRIES API_SECRET NIGHTSCOUT_HOST +# requires API_SECRET and NIGHTSCOUT_HOST to be set in calling environment (i.e. in crontab) +( +curl -s -X POST --data-binary @$ENTRIES \ + -H "API-SECRET: $API_SECRET" \ + -H "content-type: application/json" \ + $NIGHTSCOUT_HOST/api/v1/entries.json +) && ( test -n "$OUTPUT" && touch $OUTPUT ; logger "Uploaded $ENTRIES to $NIGHTSCOUT_HOST" ) || logger "Unable to upload to $NIGHTSCOUT_HOST" + diff --git a/bin/oref0-calculate-iob.js b/bin/oref0-calculate-iob.js new file mode 100755 index 0000000..19d21ef --- /dev/null +++ b/bin/oref0-calculate-iob.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +/* + Insulin On Board (IOB) calculations. + + IOB is also known as "Bolus on Board", "Active Insulin", or "Insulin Remaining" + + Released under MIT license. See the accompanying LICENSE.txt file for + full terms and conditions + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +var generate = require('oref0/lib/iob'); + +if (!module.parent) { + var iob_input = process.argv.slice(2, 3).pop() + var profile_input = process.argv.slice(3, 4).pop() + var clock_input = process.argv.slice(4, 5).pop() + + if (!iob_input || !profile_input) { + console.log('usage: ', process.argv.slice(0, 2), ' '); + process.exit(1); + } + + var cwd = process.cwd() + var all_data = require(cwd + '/' + iob_input); + var profile_data = require(cwd + '/' + profile_input); + var clock_data = require(cwd + '/' + clock_input); + + all_data.sort(function (a, b) { return a.date > b.date }); + + var inputs = { + history: all_data + , profile: profile_data + , clock: clock_data + }; + + var iob = generate(inputs); + console.log(JSON.stringify(iob)); +} + diff --git a/bin/oref0-determine-basal.js b/bin/oref0-determine-basal.js new file mode 100755 index 0000000..83a3467 --- /dev/null +++ b/bin/oref0-determine-basal.js @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +/* + Determine Basal + + Released under MIT license. See the accompanying LICENSE.txt file for + full terms and conditions + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +if (!module.parent) { + var determinebasal = init(); + + var iob_input = process.argv.slice(2, 3).pop() + var currenttemp_input = process.argv.slice(3, 4).pop() + var glucose_input = process.argv.slice(4, 5).pop() + var profile_input = process.argv.slice(5, 6).pop() + var offline = process.argv.slice(6, 7).pop() + + if (!iob_input || !currenttemp_input || !glucose_input || !profile_input) { + console.error('usage: ', process.argv.slice(0, 2), ' [Offline]'); + process.exit(1); + } + + var cwd = process.cwd() + var glucose_data = require(cwd + '/' + glucose_input); + var currenttemp = require(cwd + '/' + currenttemp_input); + var iob_data = require(cwd + '/' + iob_input); + var profile = require(cwd + '/' + profile_input); + var glucose_status = determinebasal.getLastGlucose(glucose_data); + + //if old reading from Dexcom do nothing + + var systemTime = new Date(); + var bgTime; + if (glucose_data[0].display_time) { + bgTime = new Date(glucose_data[0].display_time.replace('T', ' ')); + } else if (glucose_data[0].dateString) { + bgTime = new Date(glucose_data[0].dateString); + } else { console.error("Could not determine last BG time"); } + var minAgo = (systemTime - bgTime) / 60 / 1000 + + if (minAgo > 10 || minAgo < -5) { // Dexcom data is too old, or way in the future + var reason = "BG data is too old, or clock set incorrectly"; + console.error(reason); + return 1; + } + console.error(JSON.stringify(glucose_status)); + console.error(JSON.stringify(currenttemp)); + console.error(JSON.stringify(iob_data)); + console.error(JSON.stringify(profile)); + rT = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + +} + +function init() { + + var determinebasal = { + name: 'determine-basal' + , label: "OpenAPS Determine Basal" + , pluginType: 'pill-major' + }; + + determinebasal.getLastGlucose = function getLastGlucose(data) { + + var now = data[0]; + var last = data[1]; + var minutes; + var change; + var avg; + //TODO: calculate average using system_time instead of assuming 1 data point every 5m + if (typeof data[3] !== 'undefined' && data[3].glucose > 30) { + minutes = 3*5; + change = now.glucose - data[3].glucose; + } else if (typeof data[2] !== 'undefined' && data[2].glucose > 30) { + minutes = 2*5; + change = now.glucose - data[2].glucose; + } else if (typeof data[1] !== 'undefined' && data[1].glucose > 30) { + minutes = 1*5; + change = now.glucose - data[1].glucose; + } else { change = 0; } + // multiply by 5 to get the same units as delta, i.e. mg/dL/5m + avg = change/minutes * 5; + var o = { + delta: now.glucose - last.glucose + , glucose: now.glucose + , avgdelta: avg + }; + + return o; + + } + + + determinebasal.determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, offline) { + if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { + console.error('Error: could not get current basal rate'); + process.exit(1); + } + + var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver + + // if target_bg is set, great. otherwise, if min and max are set, then set target to their average + var target_bg; + if (typeof profile.target_bg !== 'undefined') { + target_bg = profile.target_bg; + } else { + if (typeof profile.max_bg !== 'undefined' && typeof profile.max_bg !== 'undefined') { + target_bg = (profile.min_bg + profile.max_bg) / 2; + } else { + console.error('Error: could not determine target_bg'); + process.exit(1); + } + } + + var bg = glucose_status.glucose; + var tick; + if (glucose_status.delta >= 0) { tick = "+" + glucose_status.delta; } + else { tick = glucose_status.delta; } + console.error("IOB: " + iob_data.iob.toFixed(2) + ", Bolus IOB: " + iob_data.bolusiob.toFixed(2)); + //calculate BG impact: the amount BG "should" be rising or falling based on insulin activity alone + var bgi = Math.round(( -iob_data.activity * profile.sens * 5 )*100)/100; + console.error("Avg. Delta: " + glucose_status.avgdelta.toFixed(1) + ", BGI: " + bgi.toFixed(1)); + // project deviation over next 15 minutes + var deviation = Math.round( 15 / 5 * ( glucose_status.avgdelta - bgi ) ); + console.error("15m deviation: " + deviation.toFixed(0)); + // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity + var naive_eventualBG = Math.round( bg - (iob_data.iob * profile.sens) ); + // and adjust it for the deviation above + var eventualBG = naive_eventualBG + deviation; + // calculate what portion of that is due to bolusiob + var bolusContrib = iob_data.bolusiob * profile.sens; + // and add it back in to get snoozeBG, plus another 50% to avoid low-temping at mealtime + var naive_snoozeBG = Math.round( naive_eventualBG + 1.5 * bolusContrib ); + // adjust that for deviation like we did eventualBG + var snoozeBG = naive_snoozeBG + deviation; + console.error("BG: " + bg + tick + " -> " + eventualBG + "-" + snoozeBG + " (Unadjusted: " + naive_eventualBG + "-" + naive_snoozeBG + ")"); + if (typeof eventualBG === 'undefined') { console.error('Error: could not calculate eventualBG'); } + var rT = { //short for requestedTemp + 'temp': 'absolute' + , 'bg': bg + , 'tick': tick + , 'eventualBG': eventualBG + , 'snoozeBG': snoozeBG + }; + + + if (bg < 30) { //Dexcom is in ??? mode or calibrating, do nothing. Asked @benwest for raw data in iter_glucose + rT.reason = "CGM is calibrating or in ??? state"; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + var threshold = profile.min_bg - 30; + + if (bg < threshold) { // low glucose suspend mode: BG is < ~80 + rT.reason = "BG " + bg + "<" + threshold; + if ((glucose_status.delta < 0 && glucose_status.avgdelta < 0) || (glucose_status.delta < bgi && glucose_status.avgdelta < bgi)) { + // BG is still falling / rising slower than predicted + console.error(rT.reason); + return determinebasal.setTempBasal(0, 30, profile, rT, offline); + } + if (glucose_status.delta > glucose_status.avgdelta) { + rT.reason += ", delta " + glucose_status.delta + ">0"; + } else { + rT.reason += ", avg delta " + glucose_status.avgdelta + ">0"; + } + if (currenttemp.rate > profile.current_basal) { // if a high-temp is running + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel high temp + } else if (currenttemp.duration && eventualBG > profile.max_bg) { // if low-temped and predicted to go high from negative IOB + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel low temp + } + rT.reason += "; no high-temp to cancel"; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + if (eventualBG < profile.min_bg) { // if eventual BG is below target: + rT.reason = "Eventual BG " + eventualBG + "<" + profile.min_bg; + // if 5m or 15m avg BG is rising faster than BGI/2 + if (glucose_status.delta > bgi/2 && glucose_status.avgdelta > bgi/2) { + rT.reason += ", but Delta " + tick + " > BGI " + bgi + " / 2"; + if (currenttemp.duration > 0) { // if there is currently any temp basal running + rT.reason = rT.reason += "; cancel"; + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel temp + } + rT.reason = rT.reason += "; no temp to cancel"; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + // if this is just due to boluses, we can snooze until the bolus IOB decays (at double speed) + if (snoozeBG > profile.min_bg) { // if adding back in the bolus contribution BG would be above min + // if BG is falling and high-temped, or rising and low-temped, cancel + // compare against zero here, not BGI, because BGI will be highly negative from boluses and no carbs + if (glucose_status.delta < 0 && currenttemp.rate > profile.current_basal) { + rT.reason += tick + ", and temp " + currenttemp.rate + " > basal " + profile.current_basal; + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel temp + } else if (glucose_status.delta > 0 && currenttemp.rate < profile.current_basal) { + rT.reason += tick + ", and temp " + currenttemp.rate + " < basal " + profile.current_basal; + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel temp + } + rT.reason += "bolus snooze: eventual BG range " + eventualBG + "-" + snoozeBG; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + // calculate 30m low-temp required to get projected BG up to target + // use snoozeBG instead of eventualBG to more gradually ramp in any counteraction of the user's boluses + var insulinReq = Math.min(0, (snoozeBG - target_bg) / profile.sens); + // rate required to deliver insulinReq less insulin over 30m: + var rate = profile.current_basal + (2 * insulinReq); + rate = Math.round( rate * 1000 ) / 1000; + // if required temp < existing temp basal + if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 0 && rate > currenttemp.rate - 0.1)) { + rT.reason += "temp " + currenttemp.rate + " <~ req " + rate + "U/hr"; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + rT.reason += ", no temp, setting " + rate + "U/hr"; + return determinebasal.setTempBasal(rate, 30, profile, rT, offline); + } + // if eventual BG is above min but BG is falling faster than BGI/2 + if (glucose_status.delta < bgi/2 || glucose_status.avgdelta < bgi/2) { + rT.reason = "Eventual BG " + eventualBG + ">" + profile.min_bg + " but Delta " + tick + " < BGI " + bgi + " / 2"; + if (currenttemp.duration > 0) { // if there is currently any temp basal running + rT.reason = rT.reason += "; cancel"; + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel temp + } + rT.reason = rT.reason += "; no temp to cancel"; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + if (eventualBG < profile.max_bg) { + rT.reason = eventualBG + " is in range. No temp required"; + if (currenttemp.duration > 0) { // if there is currently any temp basal running + return determinebasal.setTempBasal(0, 0, profile, rT, offline); // cancel temp + } + if (offline == 'Offline') { + // if no temp is running or required, set the current basal as a temp, so you can see on the pump that the loop is working + if ((!currenttemp.duration || (currenttemp.rate == profile.current_basal)) && !rT.duration) { + rT.reason = rT.reason + "; setting current basal of " + profile.current_basal + " as temp"; + return determinebasal.setTempBasal(profile.current_basal, 30, profile, rT, offline); + } + } + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } + + // eventual BG is above target: + // if iob is over max, just cancel any temps + var basal_iob = Math.round(( iob_data.iob - iob_data.bolusiob )*1000)/1000; + rT.reason = "Eventual BG " + eventualBG + ">" + profile.max_bg + ", "; + if (basal_iob > max_iob) { + rT.reason = "basal_iob " + basal_iob + " > max_iob " + max_iob; + return determinebasal.setTempBasal(0, 0, profile, rT, offline); + } + // otherwise, calculate 30m high-temp required to get projected BG down to target + // insulinReq is the additional insulin required to get down to max bg: + var insulinReq = (eventualBG - target_bg) / profile.sens; + // if that would put us over max_iob, then reduce accordingly + if (insulinReq > max_iob-basal_iob) { + rT.reason = "max_iob " + max_iob + ", "; + insulinReq = max_iob-basal_iob; + } + + // rate required to deliver insulinReq more insulin over 30m: + var rate = profile.current_basal + (2 * insulinReq); + rate = Math.round( rate * 1000 ) / 1000; + + maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); + if (rate > maxSafeBasal) { + rate = maxSafeBasal; + } + var insulinScheduled = currenttemp.duration * (currenttemp.rate - profile.current_basal) / 60; + if (insulinScheduled > insulinReq + 0.3) { // if current temp would deliver >0.3U more than the required insulin, lower the rate + rT.reason = currenttemp.duration + "@" + currenttemp.rate + " > req " + insulinReq + "U"; + return determinebasal.setTempBasal(rate, 30, profile, rT, offline); + } + if (typeof currenttemp.rate == 'undefined' || currenttemp.rate == 0) { // no temp is set + rT.reason += "no temp, setting " + rate + "U/hr"; + return determinebasal.setTempBasal(rate, 30, profile, rT, offline); + } + if (currenttemp.duration > 0 && rate < currenttemp.rate + 0.1) { // if required temp <~ existing temp basal + rT.reason += "temp " + currenttemp.rate + " >~ req " + rate + "U/hr"; + console.error(rT.reason); + console.log(JSON.stringify(rT)); + return rT; + } // required temp > existing temp basal + rT.reason += "temp " + currenttemp.rate + "<" + rate + "U/hr"; + return determinebasal.setTempBasal(rate, 30, profile, rT, offline); + + + } + + determinebasal.setTempBasal = function setTempBasal(rate, duration, profile, rT, offline) { + + maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); + + if (rate < 0) { rate = 0; } // if >30m @ 0 required, zero temp will be extended to 30m instead + else if (rate > maxSafeBasal) { rate = maxSafeBasal; } + + // rather than canceling temps, if Offline mode is set, always set the current basal as a 30m temp + // so we can see on the pump that openaps is working + if (duration == 0 && offline == 'Offline') { + rate = profile.current_basal; + duration = 30; + } + + rT.duration = duration; + rT.rate = Math.round((Math.round(rate / 0.05) * 0.05)*100)/100; + console.log(JSON.stringify(rT)); + return rT; + }; + + return determinebasal; + +} +module.exports = init; diff --git a/bin/oref0-get-profile.js b/bin/oref0-get-profile.js new file mode 100755 index 0000000..d5df8f3 --- /dev/null +++ b/bin/oref0-get-profile.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/* + Get Basal Information + + Released under MIT license. See the accompanying LICENSE.txt file for + full terms and conditions + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +var generate = require('oref0/lib/profile/'); + +if (!module.parent) { + + var pumpsettings_input = process.argv.slice(2, 3).pop() + var bgtargets_input = process.argv.slice(3, 4).pop() + var isf_input = process.argv.slice(4, 5).pop() + var basalprofile_input = process.argv.slice(5, 6).pop() + var carbratio_input = process.argv.slice(6, 7).pop() + var maxiob_input = process.argv.slice(7, 8).pop() + + if (!pumpsettings_input || !bgtargets_input || !isf_input || !basalprofile_input || !carbratio_input) { + console.log('usage: ', process.argv.slice(0, 2), ' []'); + process.exit(1); + } + + var cwd = process.cwd() + var pumpsettings_data = require(cwd + '/' + pumpsettings_input); + var bgtargets_data = require(cwd + '/' + bgtargets_input); + if (bgtargets_data.units !== 'mg/dL') { + console.log('BG Target data is expected to be expressed in mg/dL.' + , 'Found', bgtargets_data.units, 'in', bgtargets_input, '.'); + process.exit(2); + } + var isf_data = require(cwd + '/' + isf_input); + var basalprofile_data = require(cwd + '/' + basalprofile_input); + var carbratio_data = require(cwd + '/' + carbratio_input);; + + var maxiob_data = { max_iob: 0 }; + if (typeof maxiob_input != 'undefined') { + maxiob_data = require(cwd + '/' + maxiob_input); + } + var inputs = { + settings: pumpsettings_data + , targets: bgtargets_data + , basals: basalprofile_data + , isf: isf_data + , max_iob: maxiob_data.max_iob || 0 + , carbs: carbratio_data + + }; + + var profile = generate(inputs); + + console.log(JSON.stringify(profile)); +} diff --git a/bin/oref0-ifttt-notify b/bin/oref0-ifttt-notify new file mode 100755 index 0000000..17ea29b --- /dev/null +++ b/bin/oref0-ifttt-notify @@ -0,0 +1,98 @@ +#!/bin/bash + +self=$(basename $0) +# Something like this: +# https://maker.ifttt.com/trigger/{event}/with/key/MyKey +IFTTT_TRIGGER=${IFTTT_TRIGGER-${1}} +IFTTT_NOTIFY_USAGE="${2-pump model}" + +function help_message ( ) { +cat < + +## Setup IFTTT Account + +You need to create an account and connect to the Maker channel and the +notification channel of your choice. I use pushover as I already had the app +and it allows me more control over the notification on my phone. + +## Create an IF recipe +The trigger is the maker channel. You can customize the notification message +if you wish. + +## Get the event trigger +On the Maker channel there is a "how to trigger" link. Copy and paste the url +for the example curl command, be sure to change the event name field. +The URL, something like: +# https://maker.ifttt.com/trigger/{event}/with/key/MyKey + +You can pass the IFTTT_TRIGGER, which is the trigger URL as the first +argument, or define it as an environment variable in your crontab. + +Command line: + + $self https://maker.ifttt.com/trigger/{event}/with/key/MyKey + + +Crontab: + IFTTT_TRIGGER=https://maker.ifttt.com/trigger/{event}/with/key/MyKey + +By default $self will check that the stick works, and notify the IFTTT_TRIGGER +endpoint only if the the stick fails to check out. If the stick diagnostics +indicate the carelink stick is working, $self will run: + + openaps use $IFTTT_NOTIFY_USAGE + +You can specify which openaps use command to use in the second argument, or by +setting the IFTTT_NOTIFY_USAGE environment variable in crontab: + +Command line, note the quotes, the second term must be passed as single word. + + $self https://maker.ifttt.com/trigger/{event}/with/key/MyKey 'pump model' + +Crontab: + + IFTTT_NOTIFY_USAGE='pump model' + +Author: @audiefile +EOF + +} + + + +case $1 in +env) + echo PATH=$PATH + env + exit + ;; +help) + help_message + ;; +*) + if [[ -z "$IFTTT_TRIGGER" || -z "$IFTTT_NOTIFY_USAGE" ]] ; then + help_message + exit 1 + fi + ;; +esac + + +# check carelink is working - if not send notification +# maybe switch: + +mm-stick diagnose > /dev/null || curl -X POST $IFTTT_TRIGGER +# python -m decocare.stick $(python -m decocare.scan) >/dev/null || curl -X POST $IFTTT_TRIGGER +echo "Carelink Stick OK" + +# check carelink can talk to pump - if not send notification +model=$(openaps use $IFTTT_NOTIFY_USAGE) +echo "Model: " $model +if [ -z "$model" ]; +then + echo "Model is empty" + curl -X POST $IFTTT_TRIGGER +fi + diff --git a/bin/pebble.js b/bin/oref0-pebble.js old mode 100644 new mode 100755 similarity index 100% rename from bin/pebble.js rename to bin/oref0-pebble.js diff --git a/bin/oref0.sh b/bin/oref0.sh new file mode 100755 index 0000000..44ffa5c --- /dev/null +++ b/bin/oref0.sh @@ -0,0 +1,45 @@ +#!/bin/bash + + +self=$(basename $0) +NAME=${1-help} +shift +PROGRAM="oref0-${NAME}" +COMMAND=$(which $PROGRAM | head -n 1) + +function help_message ( ) { + cat < + + ______ ______ ______ ______ 0 +/ | | \ | | | \ | | | | +| | | | | |__| | | |---- | |---- +\_|__|_/ |_| \_\ |_|____ |_| + +Valid commands: + oref0 env - print information about environment. + oref0 pebble + oref0 ifttt-notify + oref0 get-profile + oref0 calculate-iob + oref0 determine-basal + oref0 help - this message +EOF +} + +case $NAME in +env) + echo PATH=$PATH + env + exit + ;; +help) + help_message + ;; +*) + test -n "$COMMAND" && exec $COMMAND $* + ;; +esac + + diff --git a/bin/pebble.sh b/bin/pebble.sh index f2c7e89..75e95ef 100755 --- a/bin/pebble.sh +++ b/bin/pebble.sh @@ -17,14 +17,16 @@ # THE SOFTWARE. cd ~/openaps-dev -stat -c %y clock.json | cut -c 1-19 -cat clock.json | sed 's/"//g' | sed 's/T/ /' -echo +#stat -c %y clock.json | cut -c 1-19 +#cat clock.json | sed 's/"//g' | sed 's/T/ /' +#echo share2-bridge file glucose.json.new | grep glucose diff -q glucose.json glucose.json.new && grep -q glucose glucose.json.new && rsync -tu glucose.json.new glucose.json -node ~/openaps-js/bin/iob.js pumphistory.json profile.json clock.json > iob.json.new && grep iob iob.json.new && rsync -tu iob.json.new iob.json +#TODO: consider replacing this now.json hack with an option in iob.js to use current time instead of pump time +(echo -n '"'; date -Iseconds | sed "s/[+-][0-9][0-9]00/\"/") > now.json +node ~/openaps-js/bin/iob.js pumphistory.json profile.json now.json > iob.json.new && grep iob iob.json.new && rsync -tu iob.json.new iob.json node ~/openaps-js/bin/determine-basal.js iob.json currenttemp.json glucose.json profile.json > requestedtemp.json.new && grep reason requestedtemp.json.new && rsync -tu requestedtemp.json.new requestedtemp.json node ~/openaps-js/bin/pebble.js glucose.json iob.json current_basal_profile.json currenttemp.json requestedtemp.json enactedtemp.json > /tmp/pebble-openaps.json #cat /tmp/pebble-openaps.json -grep "refresh_frequency" /tmp/pebble-openaps.json && rsync -tu /tmp/pebble-openaps.json www/openaps.json +grep "refresh_frequency" /tmp/pebble-openaps.json && rsync -tu /tmp/pebble-openaps.json /var/www/openaps.json #cat www/openaps.json diff --git a/bin/fix-dead-carelink.sh b/bin/reset-usb.sh similarity index 55% rename from bin/fix-dead-carelink.sh rename to bin/reset-usb.sh index b728725..19d5dfe 100755 --- a/bin/fix-dead-carelink.sh +++ b/bin/reset-usb.sh @@ -13,9 +13,21 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -echo "Power-cycling USB to fix dead Carelink stick" -sleep 0.1 -echo 0 > /sys/devices/platform/bcm2708_usb/buspower -sleep 1 -echo 1 > /sys/devices/platform/bcm2708_usb/buspower -sleep 2 +# Raspberry Pi 1 running Raspbian Wheezy +FILE=/sys/devices/platform/bcm2708_usb/buspower +if [ ! -e $FILE ]; then +# Raspberry Pi 2 running Raspbian Jessie + FILE=/sys/devices/platform/soc/3f980000.usb/buspower +fi +if [ -e $FILE ]; then + echo "Power-cycling USB to fix dead Carelink stick" + sleep 0.1 + echo 0 > $FILE + sleep 1 + echo 1 > $FILE + sleep 2 +else + echo "Could not find a known USB power control device. Checking /sys/devices/platform/:" + find /sys/devices/platform/* | grep buspower +fi + diff --git a/bin/sendtempbasal-Azure.js b/bin/send-tempbasal-Azure.js old mode 100644 new mode 100755 similarity index 80% rename from bin/sendtempbasal-Azure.js rename to bin/send-tempbasal-Azure.js index 5f23730..32b6038 --- a/bin/sendtempbasal-Azure.js +++ b/bin/send-tempbasal-Azure.js @@ -1,4 +1,6 @@ -/* +#!/usr/bin/env node + +/* Send Temporary Basal to Azure Copyright (c) 2015 OpenAPS Contributors @@ -21,15 +23,17 @@ if (!module.parent) { var iob_input = process.argv.slice(2, 3).pop() var enacted_temps_input = process.argv.slice(3, 4).pop() var glucose_input = process.argv.slice(4, 5).pop() - if (!iob_input || !enacted_temps_input || !glucose_input) { - console.log('usage: ', process.argv.slice(0, 2), ' '); + var webapi = process.argv.slice(5, 6).pop() + if (!iob_input || !enacted_temps_input || !glucose_input || !webapi) { + console.log('usage: ', process.argv.slice(0, 2), ' <[your_webapi].azurewebsites.net>'); process.exit(1); } } -var glucose_data = require('./' + glucose_input); -var enacted_temps = require('./' + enacted_temps_input); -var iob_data = require('./' + iob_input); +var cwd = process.cwd(); +var glucose_data = require(cwd + '/' + glucose_input); +var enacted_temps = require(cwd + '/' + enacted_temps_input); +var iob_data = require(cwd + '/' + iob_input); @@ -46,7 +50,7 @@ var data = JSON.stringify({ ); var options = { - host: '[your_webapi].azurewebsites.net', + host: webapi, port: '443', path: '/api/openapstempbasals', method: 'POST', diff --git a/lib/iob/calculate.js b/lib/iob/calculate.js new file mode 100644 index 0000000..e1d9ee8 --- /dev/null +++ b/lib/iob/calculate.js @@ -0,0 +1,45 @@ + +function iobCalc(treatment, time, dia) { + var diaratio = dia / 3; + var peak = 75 ; + var end = 180 ; + //var sens = profile_data.sens; + if (typeof time === 'undefined') { + var time = new Date(); + } + + if (treatment.insulin) { + var bolusTime=new Date(treatment.date); + var minAgo=(time-bolusTime)/1000/60 * diaratio; + + if (minAgo < 0) { + var iobContrib=0; + var activityContrib=0; + } + else if (minAgo < peak) { + var x = (minAgo/5 + 1); + var iobContrib=treatment.insulin*(1-0.001852*x*x+0.001852*x); + //var activityContrib=sens*treatment.insulin*(2/dia/60/peak)*minAgo; + var activityContrib=treatment.insulin*(2/dia/60/peak)*minAgo; + } + else if (minAgo < end) { + var x = (minAgo-peak)/5; + var iobContrib=treatment.insulin*(0.001323*x*x - .054233*x + .55556); + //var activityContrib=sens*treatment.insulin*(2/dia/60-(minAgo-peak)*2/dia/60/(60*dia-peak)); + var activityContrib=treatment.insulin*(2/dia/60-(minAgo-peak)*2/dia/60/(60*dia-peak)); + } + else { + var iobContrib=0; + var activityContrib=0; + } + return { + iobContrib: iobContrib, + activityContrib: activityContrib + }; + } + else { + return ''; + } +} + +exports = module.exports = iobCalc; diff --git a/lib/iob/history.js b/lib/iob/history.js new file mode 100644 index 0000000..70eada2 --- /dev/null +++ b/lib/iob/history.js @@ -0,0 +1,71 @@ + +function calcTempTreatments (inputs) { + var pumpHistory = inputs.history; + var tempHistory = []; + var tempBoluses = []; + var now = new Date(); + var timeZone = now.toString().match(/([-\+][0-9]+)\s/)[1] + for (var i=0; i < pumpHistory.length; i++) { + var current = pumpHistory[i]; + //if(pumpHistory[i].date < time) { + if (pumpHistory[i]._type == "Bolus") { + //console.log(pumpHistory[i]); + var temp = {}; + temp.timestamp = current.timestamp; + //temp.started_at = new Date(current.date); + temp.started_at = new Date(current.timestamp + timeZone); + //temp.date = current.date + temp.date = temp.started_at.getTime(); + temp.insulin = current.amount + tempBoluses.push(temp); + } else if (pumpHistory[i]._type == "TempBasal") { + if (current.temp == 'percent') { + continue; + } + var rate = pumpHistory[i].rate; + var date = pumpHistory[i].date; + if (i>0 && pumpHistory[i-1].date == date && pumpHistory[i-1]._type == "TempBasalDuration") { + var duration = pumpHistory[i-1]['duration (min)']; + } else if (i+1 tempHistory[i+1].date) { + tempHistory[i].duration = (tempHistory[i+1].date - tempHistory[i].date)/60/1000; + } + } + var tempBolusSize; + var now = new Date(); + var timeZone = now.toString().match(/([-\+][0-9]+)\s/)[1] + for (var i=0; i < tempHistory.length; i++) { + if (tempHistory[i].duration > 0) { + var netBasalRate = tempHistory[i].rate-profile_data.current_basal; + if (netBasalRate < 0) { tempBolusSize = -0.05; } + else { tempBolusSize = 0.05; } + var netBasalAmount = Math.round(netBasalRate*tempHistory[i].duration*10/6)/100 + var tempBolusCount = Math.round(netBasalAmount / tempBolusSize); + var tempBolusSpacing = tempHistory[i].duration / tempBolusCount; + for (var j=0; j < tempBolusCount; j++) { + var tempBolus = {}; + tempBolus.insulin = tempBolusSize; + tempBolus.date = tempHistory[i].date + j * tempBolusSpacing*60*1000; + tempBolus.created_at = new Date(tempBolus.date); + tempBoluses.push(tempBolus); + } + } + } + return [ ].concat(tempBoluses).concat(tempHistory); +} +exports = module.exports = calcTempTreatments; diff --git a/lib/iob/index.js b/lib/iob/index.js new file mode 100644 index 0000000..e50f4e6 --- /dev/null +++ b/lib/iob/index.js @@ -0,0 +1,24 @@ + +var tz = require('timezone'); +var find_insulin = require('./history'); +var calculate = require('./calculate'); +var sum = require('./total'); + +function generate (inputs) { + + var treatments = find_insulin(inputs); + treatments.sort(function (a, b) { return a.date > b.date }); + + var opts = { + treatments: treatments + , profile: inputs.profile + , calculate: calculate + }; + + var clock = new Date(tz(inputs.clock)); + + var iob = sum(opts, clock); + return iob; +} + +exports = module.exports = generate; diff --git a/lib/iob/total.js b/lib/iob/total.js new file mode 100644 index 0000000..eade937 --- /dev/null +++ b/lib/iob/total.js @@ -0,0 +1,38 @@ + +function iobTotal(opts, time) { + var iobCalc = opts.calculate; + var treatments = opts.treatments; + var profile_data = opts.profile; + var iob = 0; + var bolusiob = 0; + var activity = 0; + if (!treatments) return {}; + //if (typeof time === 'undefined') { + //var time = new Date(); + //} + + treatments.forEach(function(treatment) { + if(treatment.date < time.getTime( )) { + var dia = profile_data.dia; + var tIOB = iobCalc(treatment, time, dia); + if (tIOB && tIOB.iobContrib) iob += tIOB.iobContrib; + if (tIOB && tIOB.activityContrib) activity += tIOB.activityContrib; + // keep track of bolus IOB separately for snoozes, but decay it three times as fast + if (treatment.insulin >= 0.2 && treatment.started_at) { + var bIOB = iobCalc(treatment, time, dia*2) + //console.log(treatment); + //console.log(bIOB); + if (bIOB && bIOB.iobContrib) bolusiob += bIOB.iobContrib; + } + } + }); + + return { + iob: iob, + activity: activity, + bolusiob: bolusiob + }; +} + +exports = module.exports = iobTotal; + diff --git a/lib/medtronic-clock.js b/lib/medtronic-clock.js new file mode 100644 index 0000000..5c319b6 --- /dev/null +++ b/lib/medtronic-clock.js @@ -0,0 +1,12 @@ + +function getTime(minutes) { + var baseTime = new Date(); + baseTime.setHours('00'); + baseTime.setMinutes('00'); + baseTime.setSeconds('00'); + + return baseTime.getTime() + minutes * 60 * 1000; +} + +exports = module.exports = getTime; + diff --git a/lib/profile/basal.js b/lib/profile/basal.js new file mode 100644 index 0000000..6cbda11 --- /dev/null +++ b/lib/profile/basal.js @@ -0,0 +1,36 @@ + +var getTime = require('../medtronic-clock'); + +/* Return basal rate(U / hr) at the provided timeOfDay */ +function basalLookup (schedules) { + var basalprofile_data = schedules; + var now = new Date(); + var basalRate = basalprofile_data[basalprofile_data.length-1].rate + + for (var i = 0; i < basalprofile_data.length - 1; i++) { + if ((now >= getTime(basalprofile_data[i].minutes)) && (now < getTime(basalprofile_data[i + 1].minutes))) { + basalRate = basalprofile_data[i].rate; + break; + } + } + return Math.round(basalRate*1000)/1000; +} + + +function maxDailyBasal (inputs) { + var basalprofile_data = inputs.basals; + basalprofile_data.sort(function (a, b) { if (a.rate < b.rate) { return 1 } if (a.rate > b.rate) { return -1; } return 0; }); + return Math.round( basalprofile_data[0].rate *1000)/1000; +} + +/*Return maximum daily basal rate(U / hr) from profile.basals */ + +function maxBasalLookup (inputs) { + + return inputs.settings.maxBasal; +} + + +exports.maxDailyBasal = maxDailyBasal; +exports.maxBasalLookup = maxBasalLookup; +exports.basalLookup = basalLookup; diff --git a/lib/profile/carbs.js b/lib/profile/carbs.js new file mode 100644 index 0000000..74cc503 --- /dev/null +++ b/lib/profile/carbs.js @@ -0,0 +1,21 @@ + +var getTime = require('../medtronic-clock'); + +function carbRatioLookup (inputs) { + var now = new Date(); + var carbratio_data = inputs.carbs; + //carbratio_data.schedule.sort(function (a, b) { return a.offset > b.offset }); + var carbRatio = carbratio_data.schedule[carbratio_data.schedule.length - 1] + + for (var i = 0; i < carbratio_data.schedule.length - 1; i++) { + if ((now >= getTime(carbratio_data.schedule[i].offset)) && (now < getTime(carbratio_data.schedule[i + 1].offset))) { + carbRatio = carbratio_data.schedule[i]; + break; + } + } + return carbRatio.ratio; + profile.carbratio = carbRatio.ratio; +} + +carbRatioLookup.carbRatioLookup = carbRatioLookup; +exports = module.exports = carbRatioLookup; diff --git a/lib/profile/index.js b/lib/profile/index.js new file mode 100644 index 0000000..f601b6b --- /dev/null +++ b/lib/profile/index.js @@ -0,0 +1,43 @@ + +var basal = require('./basal'); +var targets = require('./targets'); +var carbs = require('./carbs'); +var isf = require('./isf'); + +function defaults ( ) { + var profile = { + max_iob: 0 // if max_iob.json is not profided, never give more insulin than the pump would have + // , dia: pumpsettings_data.insulin_action_curve + , type: "current" + }; + return profile; +} + +function generate (inputs, opts) { + var profile = opts && opts.type ? opts : defaults( ); + + if (inputs.settings.insulin_action_curve) { + profile.dia = pumpsettings_data.insulin_action_curve; + } + + if (inputs.max_iob) { + profile.max_iob = inputs.max_iob; + } + + profile.current_basal = basal.basalLookup(inputs.basals); + profile.max_daily_basal = basal.maxDailyBasal(inputs); + profile.max_basal = basal.maxBasalLookup(inputs); + + var range = targets.bgTargetsLookup(inputs); + profile.min_bg = range.min_bg; + profile.max_bg = range.max_bg; + profile.carbratio = carbs.carbRatioLookup(inputs); + profile.sens = isf.isfLookup(inputs); + + return profile; +} + + +generate.defaults = defaults; +exports = module.exports = generate; + diff --git a/lib/profile/isf.js b/lib/profile/isf.js new file mode 100644 index 0000000..e97504f --- /dev/null +++ b/lib/profile/isf.js @@ -0,0 +1,21 @@ + +var getTime = require('../medtronic-clock'); + +function isfLookup (inputs) { + var now = new Date(); + var isf_data = inputs.isf; + //isf_data.sensitivities.sort(function (a, b) { return a.offset > b.offset }); + var isfSchedule = isf_data.sensitivities[isf_data.sensitivities.length - 1] + + for (var i = 0; i < isf_data.sensitivities.length - 1; i++) { + if ((now >= getTime(isf_data.sensitivities[i].offset)) && (now < getTime(isf_data.sensitivities[i + 1].offset))) { + isfSchedule = isf_data.sensitivities[i]; + break; + } + } + return isfSchedule.sensitivity; +} + +isfLookup.isfLookup = isfLookup; +exports = module.exports = isfLookup; + diff --git a/lib/profile/targets.js b/lib/profile/targets.js new file mode 100644 index 0000000..b3e7a2a --- /dev/null +++ b/lib/profile/targets.js @@ -0,0 +1,39 @@ + +var getTime = require('../medtronic-clock'); + +function bgTargetsLookup (inputs) { + return bound_target_range(lookup(inputs)); +} + +function lookup (inputs) { + var bgtargets_data = inputs.targets; + var now = new Date(); + + //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset }); + + var bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1] + + for (var i = 0; i < bgtargets_data.targets.length - 1; i++) { + if ((now >= getTime(bgtargets_data.targets[i].offset)) && (now < getTime(bgtargets_data.targets[i + 1].offset))) { + bgTargets = bgtargets_data.targets[i]; + break; + } + } + + return bgTargets; +} + +function bound_target_range (target) { + // hard-code lower bounds for min_bg and max_bg in case pump is set too low, or units are wrong + target.max_bg = Math.max(100, target.high); + target.min_bg = Math.max(90, target.low); + // hard-code upper bound for min_bg in case pump is set too high + target.min_bg = Math.min(200, target.min_bg); + return target +} + +bgTargetsLookup.bgTargetsLookup = bgTargetsLookup; +bgTargetsLookup.lookup = lookup; +bgTargetsLookup.bound_target_range = bound_target_range; +exports = module.exports = bgTargetsLookup; + diff --git a/package.json b/package.json index 33fe6a0..8a42f86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "openaps-js", - "version": "0.0.6", + "name": "oref0", + "version": "0.0.8", "description": "openaps js plugins", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -17,11 +17,28 @@ "bugs": { "url": "https://github.com/openaps/openaps-js/issues" }, - "bin" : { - "calculate-iob" : "./bin/iob.js", - "determine-basal" : "./bin/determine-basal.js", - "send-tempbasal-Azure" : "./bin/sendtempbasal-Azure.js", - "get-profile" : "./bin/getprofile.js" + "bin": { + "oref0-calculate-iob": "./bin/oref0-calculate-iob.js", + "oref0-determine-basal": "./bin/oref0-determine-basal.js", + "send-tempbasal-Azure": "./bin/send-tempbasal-Azure.js", + "oref0-get-profile": "./bin/oref0-get-profile.js", + "oref0-ifttt-notify": "./bin/oref0-ifttt-notify", + "oref0-reset-usb": "bin/reset-usb.sh", + "mm-format-ns-glucose": "./bin/mm-format-ns-glucose.sh", + "mm-format-ns-pump-history": "./bin/mm-format-ns-pump-history.sh", + "oref0": "./bin/oref0.sh", + "mm-stick": "./bin/mm-stick.sh", + "openaps-js": "./bin/oref0.sh", + "ns-upload-entries": "./bin/ns-upload-entries.sh", + "oref0-pebble": "./bin/oref0-pebble.js" }, - "homepage": "https://github.com/openaps/openaps-js" + "homepage": "https://github.com/openaps/openaps-js", + "dependencies": { + "share2nightscout-bridge": "^0.1.5", + "timezone": "0.0.47" + }, + "devDependencies": { + "mocha": "^2.3.3", + "should": "^7.1.0" + } } diff --git a/tests/determine-basal.test.js b/tests/determine-basal.test.js new file mode 100644 index 0000000..f72970e --- /dev/null +++ b/tests/determine-basal.test.js @@ -0,0 +1,213 @@ +'use strict'; + +require('should'); + +describe('setTempBasal', function ( ) { + var determinebasal = require('../bin/oref0-determine-basal')(); + + //function setTempBasal(rate, duration, profile, requestedTemp) + + var profile = { "current_basal":0.8,"max_daily_basal":1.3,"max_basal":3.0 }; + var rt = {}; + it('should cancel temp', function () { + var requestedTemp = determinebasal.setTempBasal(0, 0, profile, rt); + requestedTemp.rate.should.equal(0); + requestedTemp.duration.should.equal(0); + }); + + it('should set zero temp', function () { + var requestedTemp = determinebasal.setTempBasal(0, 30, profile, rt); + requestedTemp.rate.should.equal(0); + requestedTemp.duration.should.equal(30); + }); + + it('should set high temp', function () { + var requestedTemp = determinebasal.setTempBasal(2, 30, profile, rt); + requestedTemp.rate.should.equal(2); + requestedTemp.duration.should.equal(30); + }); + + it('should limit high temp to max_basal', function () { + var requestedTemp = determinebasal.setTempBasal(4, 30, profile, rt); + requestedTemp.rate.should.equal(3); + requestedTemp.duration.should.equal(30); + }); + + it('should set current_basal as temp on requestedTemp if offline', function () { + var requestedTemp = determinebasal.setTempBasal(0, 0, profile, rt, "Offline"); + requestedTemp.rate.should.equal(0.8); + requestedTemp.duration.should.equal(30); + }); + + it('should limit high temp to 3 * max_daily_basal', function () { + var profile = { "current_basal":1.0,"max_daily_basal":1.3,"max_basal":10.0 }; + var requestedTemp = determinebasal.setTempBasal(6, 30, profile, rt); + requestedTemp.rate.should.equal(3.9); + requestedTemp.duration.should.equal(30); + }); + + it('should limit high temp to 4 * current_basal', function () { + var profile = { "current_basal":0.7,"max_daily_basal":1.3,"max_basal":10.0 }; + var requestedTemp = determinebasal.setTempBasal(6, 30, profile, rt); + requestedTemp.rate.should.equal(2.8); + requestedTemp.duration.should.equal(30); + }); + +}); + +describe('determine-basal', function ( ) { + var determinebasal = require('../bin/oref0-determine-basal')(); + + //function determine_basal(glucose_status, currenttemp, iob_data, profile) + + // standard initial conditions for all determine-basal test cases unless overridden + var glucose_status = {"delta":0,"glucose":115,"avgdelta":0}; + var currenttemp = {"duration":0,"rate":0,"temp":"absolute"}; + var iob_data = {"iob":0,"activity":0,"bolusiob":0}; + var profile = {"max_iob":1.5,"dia":3,"type":"current","current_basal":0.9,"max_daily_basal":1.3,"max_basal":3.5,"max_bg":120,"min_bg":110,"sens":40}; + + it('should do nothing when in range w/o IOB', function () { + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + (typeof output.rate).should.equal('undefined'); + (typeof output.duration).should.equal('undefined'); + output.reason.should.match(/in range/); + }); + + it('should set current temp when in range w/o IOB with Offline set', function () { + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile, 'Offline'); + output.rate.should.equal(0.9); + output.duration.should.equal(30); + output.reason.should.match(/in range.*setting current basal/); + }); + + // low glucose suspend test cases + it('should temp to 0 when low w/o IOB', function () { + var glucose_status = {"delta":-5,"glucose":75,"avgdelta":-5}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.equal(0); + output.duration.should.equal(30); + output.reason.should.match(/BG 75<80/); + }); + + it('should do nothing when low and rising w/o IOB', function () { + var glucose_status = {"delta":5,"glucose":75,"avgdelta":5}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + (typeof output.rate).should.equal('undefined'); + (typeof output.duration).should.equal('undefined'); + output.reason.should.match(/75<80.*no high-temp/); + }); + + it('should do nothing when low and rising w/ negative IOB', function () { + var glucose_status = {"delta":5,"glucose":75,"avgdelta":5}; + var iob_data = {"iob":-1,"activity":-0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + (typeof output.rate).should.equal('undefined'); + (typeof output.duration).should.equal('undefined'); + output.reason.should.match(/75<80.*no high-temp/); + }); + + it('should do nothing on uptick even if avgdelta is still negative', function () { + var glucose_status = {"delta":1,"glucose":75,"avgdelta":-2}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + (typeof output.rate).should.equal('undefined'); + (typeof output.duration).should.equal('undefined'); + output.reason.should.match(/BG 75<80/); + }); + + it('should temp to 0 when rising slower than BGI', function () { + var glucose_status = {"delta":1,"glucose":75,"avgdelta":1}; + var iob_data = {"iob":-1,"activity":-0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.equal(0); + output.duration.should.equal(30); + output.reason.should.match(/BG 75<80/); + }); + + it('should temp to 0 when low and falling, regardless of BGI', function () { + var glucose_status = {"delta":-1,"glucose":75,"avgdelta":-1}; + var iob_data = {"iob":1,"activity":0.01,"bolusiob":0.5}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.equal(0); + output.duration.should.equal(30); + output.reason.should.match(/BG 75<80/); + }); + + it('should cancel high-temp when low and rising faster than BGI', function () { + var currenttemp = {"duration":20,"rate":2,"temp":"absolute"}; + var glucose_status = {"delta":5,"glucose":75,"avgdelta":5}; + var iob_data = {"iob":-1,"activity":-0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.equal(0); + output.duration.should.equal(0); + output.reason.should.match(/BG 75<80/); + }); + + it('should high-temp when > 80-ish and rising w/ lots of negative IOB', function () { + var glucose_status = {"delta":5,"glucose":85,"avgdelta":5}; + var iob_data = {"iob":-1,"activity":-0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.be.above(1); + output.duration.should.equal(30); + output.reason.should.match(/no temp, setting/); + }); + + // low eventualBG + + it('should low-temp when eventualBG < min_bg', function () { + var glucose_status = {"delta":-3,"glucose":110,"avgdelta":-1}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.be.below(0.8); + output.duration.should.equal(30); + output.reason.should.match(/Eventual BG .*<110/); + }); + + it('should cancel low-temp when lowish and rising faster than BGI', function () { + var currenttemp = {"duration":20,"rate":0.5,"temp":"absolute"}; + var glucose_status = {"delta":3,"glucose":85,"avgdelta":3}; + var iob_data = {"iob":-0.5,"activity":-0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.equal(0); + output.duration.should.equal(0); + output.reason.should.match(/Eventual BG.*<.*but Delta.*> BGI.*; cancel/); + }); + + it('should low-temp when low and rising slower than BGI', function () { + var glucose_status = {"delta":1,"glucose":85,"avgdelta":1}; + var iob_data = {"iob":-0.5,"activity":-0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.be.below(0.8); + output.duration.should.equal(30); + output.reason.should.match(/no temp, setting/); + }); + + // high eventualBG + + it('should high-temp when eventualBG > max_bg', function () { + var glucose_status = {"delta":+3,"glucose":120,"avgdelta":+1}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.be.above(1); + output.duration.should.equal(30); + output.reason.should.match(/Eventual BG .*>120/); + }); + + it('should cancel high-temp when high and falling faster than BGI', function () { + var currenttemp = {"duration":20,"rate":2,"temp":"absolute"}; + var glucose_status = {"delta":-5,"glucose":175,"avgdelta":-5}; + var iob_data = {"iob":1,"activity":0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.equal(0); + output.duration.should.equal(0); + output.reason.should.match(/Eventual BG.*>.*but Delta.*< BGI.*; cancel/); + }); + + it('should high-temp when high and falling slower than BGI', function () { + var glucose_status = {"delta":-1,"glucose":175,"avgdelta":-1}; + var iob_data = {"iob":1,"activity":0.01,"bolusiob":0}; + var output = determinebasal.determine_basal(glucose_status, currenttemp, iob_data, profile); + output.rate.should.be.above(1); + output.duration.should.equal(30); + output.reason.should.match(/no temp, setting/); + }); + + +});