From a5088584a240d2b92ebe8f027048cd62d4512f49 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 10 Jun 2016 18:50:46 +0200 Subject: [PATCH] OpenAPS MA initial work --- .idea/misc.xml | 2 +- app/build.gradle | 1 + .../main/assets/OpenAPSMA/determine-basal.js | 316 ++++++++++++++++++ .../info/nightscout/androidaps/Config.java | 1 + .../nightscout/androidaps/MainActivity.java | 4 + .../info/nightscout/androidaps/data/Iob.java | 4 - .../info/nightscout/androidaps/data/Pump.java | 6 + .../androidaps/db/DatabaseHelper.java | 61 ++++ .../nightscout/androidaps/db/TempBasal.java | 17 +- .../nightscout/androidaps/db/Treatment.java | 36 +- .../androidaps/plugins/APSBase.java | 13 + .../androidaps/plugins/APSResult.java | 11 + .../LowSuspend/LowSuspendFragment.java | 53 +++ .../plugins/LowSuspend/LowSuspendResult.java | 25 ++ .../OpenAPSMA/DetermineBasalAdapterJS.java | 244 ++++++++++++++ .../OpenAPSMA/DetermineBasalResult.java | 41 +++ .../plugins/OpenAPSMA/IobTotal.java | 77 +++++ .../plugins/OpenAPSMA/OpenAPSMAFragment.java | 209 ++++++++++++ .../plugins/Overview/OverviewFragment.java | 2 - .../androidaps/plugins/PluginBase.java | 2 + .../androidaps/plugins/ScriptReader.java | 41 +++ .../TempBasals/TempBasalsFragment.java | 21 +- .../Dialogs/NewTreatmentDialogFragment.java | 4 +- .../Dialogs/WizardDialogFragment.java | 23 +- .../Treatments/TreatmentsFragment.java | 60 +++- .../nightscout/client/data/NSProfile.java | 7 + .../main/res/layout/lowsuspend_fragment.xml | 13 + .../main/res/layout/openapsma_fragment.xml | 19 ++ .../main/res/layout/tempbasals_fragment.xml | 14 - app/src/main/res/layout/tempbasals_item.xml | 20 -- app/src/main/res/values/strings.xml | 3 +- 31 files changed, 1235 insertions(+), 115 deletions(-) create mode 100644 app/src/main/assets/OpenAPSMA/determine-basal.js create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/APSBase.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/APSResult.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendFragment.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendResult.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterJS.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalResult.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/IobTotal.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAFragment.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/ScriptReader.java create mode 100644 app/src/main/res/layout/lowsuspend_fragment.xml create mode 100644 app/src/main/res/layout/openapsma_fragment.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 5d19981032..fbb68289f4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -37,7 +37,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 797ac55410..0ec3e3453f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,4 +32,5 @@ dependencies { compile 'com.github.tony19:logback-android-classic:1.1.1-4' compile 'org.slf4j:slf4j-api:1.7.12' compile 'com.jjoe64:graphview:4.0.1' + compile 'com.eclipsesource.j2v8:j2v8:3.1.6@aar' } diff --git a/app/src/main/assets/OpenAPSMA/determine-basal.js b/app/src/main/assets/OpenAPSMA/determine-basal.js new file mode 100644 index 0000000000..1492caf3de --- /dev/null +++ b/app/src/main/assets/OpenAPSMA/determine-basal.js @@ -0,0 +1,316 @@ +/* + 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. +*/ +var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, offline, meal_data, setTempBasal) { + var rT = { //short for requestedTemp + }; + + if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { + rT.error ='Error: could not get current basal rate'; + return rT; + } + + var bg = glucose_status.glucose; + if (bg < 30) { //Dexcom is in ??? mode or calibrating, do nothing. Asked @benwest for raw data in iter_glucose + rT.error = "CGM is calibrating or in ??? state"; + return rT; + } + + 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.min_bg !== 'undefined' && typeof profile.max_bg !== 'undefined') { + target_bg = (profile.min_bg + profile.max_bg) / 2; + } else { + rT.error ='Error: could not determine target_bg'; + return rT; + } + } + + + if (typeof iob_data === 'undefined' ) { + rT.error ='Error: iob_data undefined'; + return rT; + } + + if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' || typeof iob_data.activity === 'undefined') { + rT.error ='Error: iob_data missing some property'; + return rT; + } + + var tick; + + if (glucose_status.delta >= 0) { + tick = "+" + glucose_status.delta; + } else { + tick = glucose_status.delta; + } + var minDelta = Math.min(glucose_status.delta, glucose_status.avgdelta); + //var maxDelta = Math.max(glucose_status.delta, glucose_status.avgdelta); + + + //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; + // project positive deviations for 15 minutes + var deviation = Math.round( 15 / 5 * ( glucose_status.avgdelta - bgi ) ); + // project negative deviations for 30 minutes + if (deviation < 0) { + deviation = Math.round( 30 / 5 * ( glucose_status.avgdelta - bgi ) ); + } + //console.log("Avg.Delta: " + glucose_status.avgdelta.toFixed(1) + ", BGI: " + bgi.toFixed(1) + " 15m activity projection: " + 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 bolussnooze + var bolusContrib = iob_data.bolussnooze * 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.log("BG: " + bg +"(" + tick + ","+glucose_status.avgdelta.toFixed(1)+")"+ " -> " + eventualBG + "-" + snoozeBG + " (Unadjusted: " + naive_eventualBG + "-" + naive_snoozeBG + "), BGI: " + bgi); + + var expectedDelta = Math.round(( bgi + ( target_bg - eventualBG ) / ( profile.dia * 60 / 5 ) )*10)/10; + //console.log("expectedDelta: " + expectedDelta); + + if (typeof eventualBG === 'undefined' || isNaN(eventualBG)) { + rT.error ='Error: could not calculate eventualBG'; + return rT; + } + + // min_bg of 90 -> threshold of 70, 110 -> 80, and 130 -> 90 + var threshold = profile.min_bg - 0.5*(profile.min_bg-50); + + rT = { + 'temp': 'absolute' + , 'bg': bg + , 'tick': tick + , 'eventualBG': eventualBG + , 'snoozeBG': snoozeBG + }; + + var basaliob; + if (iob_data.basaliob) { basaliob = iob_data.basaliob; } + else { basaliob = iob_data.iob - iob_data.bolussnooze; } + // allow meal assist to run when carbs are just barely covered + if (minDelta > Math.max(3, bgi) && ( (meal_data.carbs > 0 && (1.1 * meal_data.carbs/profile.carb_ratio > meal_data.boluses + basaliob)) || ( deviation > 25 && minDelta > 7 ) ) ) { + // ignore all covered IOB, and just set eventualBG to the current bg + eventualBG = Math.max(bg,eventualBG) + deviation; + rT.eventualBG = eventualBG; + profile.min_bg = 80; + target_bg = (profile.min_bg + profile.max_bg) / 2; + expectedDelta = Math.round(( bgi + ( target_bg - eventualBG ) / ( profile.dia * 60 / 5 ) )*10)/10; + rT.mealAssist = "On: Carbs: " + meal_data.carbs + " Boluses: " + meal_data.boluses + " Target: " + target_bg + " Deviation: " + deviation + " BGI: " + bgi; + } else { + rT.mealAssist = "Off: Carbs: " + meal_data.carbs + " Boluses: " + meal_data.boluses + " Target: " + target_bg + " Deviation: " + deviation + " BGI: " + bgi; + } + 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 < expectedDelta && glucose_status.avgdelta < expectedDelta)) { + // BG is still falling / rising slower than predicted + return 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.toFixed(2) + ">0"; + } + if (currenttemp.rate > profile.current_basal) { // if a high-temp is running + rT.reason += ", cancel high temp"; + return 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 + rT.reason += ", cancel low temp"; + return setTempBasal(0, 0, profile, rT, offline); // cancel low temp + } + rT.reason += "; no high-temp to cancel"; + return rT; + } + if (eventualBG < profile.min_bg) { // if eventual BG is below target: + if (rT.mealAssist.indexOf("On") == 0) { + rT.reason = "Meal assist: " + meal_data.carbs + "g, " + meal_data.boluses + "U"; + } else { + rT.reason = "Eventual BG " + eventualBG + "<" + profile.min_bg; + // if 5m or 15m avg BG is rising faster than expected delta + if (minDelta > expectedDelta && minDelta > 0) { + if (glucose_status.delta > glucose_status.avgdelta) { + rT.reason += ", but Delta " + tick + " > Exp. Delta " + expectedDelta; + } else { + rT.reason += ", but Avg. Delta " + glucose_status.avgdelta.toFixed(2) + " > Exp. Delta " + expectedDelta; + } + if (currenttemp.duration > 0) { // if there is currently any temp basal running + rT.reason = rT.reason += "; cancel"; + return setTempBasal(0, 0, profile, rT, offline); // cancel temp + } else { + rT.reason = rT.reason += "; no temp to cancel"; + return rT; + } + } + } + + if (eventualBG < profile.min_bg) { + // 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.duration > 0 && currenttemp.rate > profile.current_basal) { + rT.reason += tick + ", and temp " + currenttemp.rate + " > basal " + profile.current_basal; + return setTempBasal(0, 0, profile, rT, offline); // cancel temp + } else if (glucose_status.delta > 0 && currenttemp.duration > 0 && currenttemp.rate < profile.current_basal) { + rT.reason += tick + ", and temp " + currenttemp.rate + " < basal " + profile.current_basal; + return setTempBasal(0, 0, profile, rT, offline); // cancel temp + } + + rT.reason += ", bolus snooze: eventual BG range " + eventualBG + "-" + snoozeBG; + return rT; + } 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) / profile.sens); + if (minDelta < 0 && minDelta > expectedDelta) { + // if we're barely falling, newinsulinReq should be barely negative + rT.reason += ", Snooze BG " + snoozeBG; + var newinsulinReq = Math.round(( insulinReq * (minDelta / expectedDelta) ) * 100)/100; + //console.log("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq); + insulinReq = newinsulinReq; + } + // 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 + var insulinScheduled = currenttemp.duration * (currenttemp.rate - profile.current_basal) / 60; + if (insulinScheduled < insulinReq - 0.2) { // if current temp would deliver >0.2U less than the required insulin, raise the rate + rT.reason = currenttemp.duration + "m@" + (currenttemp.rate - profile.current_basal).toFixed(3) + " = " + insulinScheduled.toFixed(3) + " < req " + insulinReq + "-0.2U"; + return setTempBasal(rate, 30, profile, rT, offline); + } + if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate > currenttemp.rate - 0.1)) { + rT.reason += ", temp " + currenttemp.rate + " ~< req " + rate + "U/hr"; + return rT; + } else { + rT.reason += ", setting " + rate + "U/hr"; + return setTempBasal(rate, 30, profile, rT, offline); + } + } + } + } + + // if eventual BG is above min but BG is falling faster than expected Delta + if (minDelta < expectedDelta) { + if (glucose_status.delta < glucose_status.avgdelta) { + rT.reason = "Eventual BG " + eventualBG + ">" + profile.min_bg + " but Delta " + tick + " < Exp. Delta " + expectedDelta; + } else { + rT.reason = "Eventual BG " + eventualBG + ">" + profile.min_bg + " but Avg. Delta " + glucose_status.avgdelta.toFixed(2) + " < Exp. Delta " + expectedDelta; + } + if (currenttemp.duration > 0) { // if there is currently any temp basal running + rT.reason = rT.reason += "; cancel"; + return setTempBasal(0, 0, profile, rT, offline); // cancel temp + } else { + rT.reason = rT.reason += "; no temp to cancel"; + 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 + rT.reason = rT.reason += "; cancel"; + return 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 setTempBasal(profile.current_basal, 30, profile, rT, offline); + } + } + return rT; + } + + if (snoozeBG < profile.max_bg) { + rT.reason = snoozeBG + " < " + profile.max_bg; + if (currenttemp.duration > 0) { // if there is currently any temp basal running + rT.reason = rT.reason += "; cancel"; + return setTempBasal(0, 0, profile, rT, offline); // cancel temp + } else { + rT.reason = rT.reason += "; no temp to cancel"; + return rT; + } + } + + + // eventual BG is at/above target: + // if iob is over max, just cancel any temps + var basaliob; + if (iob_data.basaliob) { basaliob = iob_data.basaliob; } + else { basaliob = iob_data.iob - iob_data.bolussnooze; } + rT.reason = "Eventual BG " + eventualBG + ">=" + profile.max_bg + ", "; + if (basaliob > max_iob) { + rT.reason = "basaliob " + basaliob + " > max_iob " + max_iob; + return setTempBasal(0, 0, profile, rT, offline); + } else { // 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: + // if in meal assist mode, check if snoozeBG is lower, as eventualBG is not dependent on IOB + var insulinReq = (Math.min(snoozeBG,eventualBG) - target_bg) / profile.sens; + if (minDelta < 0 && minDelta > expectedDelta) { + var newinsulinReq = Math.round(( insulinReq * (1 - (minDelta / expectedDelta)) ) * 100)/100; + //console.log("Reducing insulinReq from " + insulinReq + " to " + newinsulinReq); + insulinReq = newinsulinReq; + } + // if that would put us over max_iob, then reduce accordingly + if (insulinReq > max_iob-basaliob) { + rT.reason = "max_iob " + max_iob + ", "; + insulinReq = max_iob-basaliob; + } + + // rate required to deliver insulinReq more insulin over 30m: + var rate = profile.current_basal + (2 * insulinReq); + rate = Math.round( rate * 1000 ) / 1000; + + var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); + if (rate > maxSafeBasal) { + rT.reason += "adj. req. rate:"+rate.toFixed(1) +" to maxSafeBasal:"+maxSafeBasal.toFixed(1)+", "; + rate = maxSafeBasal; + } + + var insulinScheduled = currenttemp.duration * (currenttemp.rate - profile.current_basal) / 60; + if (insulinScheduled > insulinReq + 0.2) { // if current temp would deliver >0.2U more than the required insulin, lower the rate + rT.reason = currenttemp.duration + "m@" + (currenttemp.rate - profile.current_basal).toFixed(3) + " = " + insulinScheduled.toFixed(3) + " > req " + insulinReq + "+0.2U"; + return setTempBasal(rate, 30, profile, rT, offline); + } + + if (typeof currenttemp.duration == 'undefined' || currenttemp.duration == 0) { // no temp is set + rT.reason += "no temp, setting " + rate + "U/hr"; + return setTempBasal(rate, 30, profile, rT, offline); + } + + if (currenttemp.duration > 5 && rate < currenttemp.rate + 0.1) { // if required temp <~ existing temp basal + rT.reason += "temp " + currenttemp.rate + " >~ req " + rate + "U/hr"; + return rT; + } + + // required temp > existing temp basal + rT.reason += "temp " + currenttemp.rate + "<" + rate + "U/hr"; + return setTempBasal(rate, 30, profile, rT, offline); + } + +}; + +module.exports = determine_basal; \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/Config.java b/app/src/main/java/info/nightscout/androidaps/Config.java index b4f54dae85..5e0f2b84e2 100644 --- a/app/src/main/java/info/nightscout/androidaps/Config.java +++ b/app/src/main/java/info/nightscout/androidaps/Config.java @@ -8,4 +8,5 @@ public class Config { public static final boolean logFunctionCalls = true; public static final boolean logIncommingBG = true; public static final boolean logIncommingData = true; + public static final boolean logAPSResult = true; } diff --git a/app/src/main/java/info/nightscout/androidaps/MainActivity.java b/app/src/main/java/info/nightscout/androidaps/MainActivity.java index 4ee183a2bf..1c02076fb1 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/MainActivity.java @@ -10,6 +10,8 @@ import android.view.MenuItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import info.nightscout.androidaps.plugins.LowSuspend.LowSuspendFragment; +import info.nightscout.androidaps.plugins.OpenAPSMA.OpenAPSMAFragment; import info.nightscout.androidaps.plugins.Overview.OverviewFragment; import info.nightscout.androidaps.plugins.ProfileViewer.ProfileViewerFragment; import info.nightscout.androidaps.plugins.TempBasals.TempBasalsFragment; @@ -37,6 +39,8 @@ public class MainActivity extends AppCompatActivity { // Register all tabs in app here mAdapter = new TabPageAdapter(getSupportFragmentManager()); mAdapter.registerNewFragment("Overview", OverviewFragment.newInstance()); + mAdapter.registerNewFragment("LowSuspend", LowSuspendFragment.newInstance()); + mAdapter.registerNewFragment("OpenAPS MA", OpenAPSMAFragment.newInstance()); mAdapter.registerNewFragment("Treatments", treatmentsFragment = TreatmentsFragment.newInstance()); mAdapter.registerNewFragment("TempBasals", tempBasalsFragment = TempBasalsFragment.newInstance()); mAdapter.registerNewFragment("Profile", ProfileViewerFragment.newInstance()); diff --git a/app/src/main/java/info/nightscout/androidaps/data/Iob.java b/app/src/main/java/info/nightscout/androidaps/data/Iob.java index 92fdae4f7c..ee70699604 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/Iob.java +++ b/app/src/main/java/info/nightscout/androidaps/data/Iob.java @@ -6,14 +6,10 @@ package info.nightscout.androidaps.data; public class Iob { public double iobContrib = 0d; public double activityContrib = 0d; - public double netInsulin = 0d; // for calculations from temp basals only - public double netRatio = 0d; // for calculations from temp basals only public Iob plus(Iob iob) { iobContrib += iob.iobContrib; activityContrib += iob.activityContrib; - netInsulin += iob.netInsulin; - netRatio += iob.netRatio; return this; } } diff --git a/app/src/main/java/info/nightscout/androidaps/data/Pump.java b/app/src/main/java/info/nightscout/androidaps/data/Pump.java index b17739e1fa..5c2cc10619 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/Pump.java +++ b/app/src/main/java/info/nightscout/androidaps/data/Pump.java @@ -5,6 +5,12 @@ package info.nightscout.androidaps.data; */ public abstract class Pump { + boolean tempBasalInProgress = false; + // Upload to pump new basal profile from MainApp.getNSProfile() public abstract void setNewBasalProfile(); + + public abstract double getBaseBasalRate(); // base basal rate, not temp basal + public abstract double getTempBasalAbsoluteRate(); + public abstract double getTempBasalRemainingMinutes(); } diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index c3003ac9fd..0b7f7a944c 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -160,4 +160,65 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { return new ArrayList(); } + /* + * Returns glucose_status for openAPS or null if no actual data available + */ + public class GlucoseStatus { + public double glucose = 0d; + public double delta = 0d; + public double avgdelta = 0d; + } + + public GlucoseStatus getGlucoseStatusData() { + GlucoseStatus result = new GlucoseStatus(); + try { + + Dao daoBgreadings = null; + daoBgreadings = getDaoBgReadings(); + List bgReadings; + QueryBuilder queryBuilder = daoBgreadings.queryBuilder(); + queryBuilder.orderBy("timeIndex", false); + queryBuilder.limit(4l); + PreparedQuery preparedQuery = queryBuilder.prepare(); + bgReadings = daoBgreadings.query(preparedQuery); + + int sizeRecords = bgReadings.size(); + + if (sizeRecords < 4 || bgReadings.get(sizeRecords - 1).timestamp > new Date().getTime() - 7 * 60 * 1000l) + return null; + + int minutes = 5; + double change; + double avg; + + if (bgReadings.size() > 3) { + BgReading now = bgReadings.get(sizeRecords - 1); + BgReading last = bgReadings.get(sizeRecords - 2); + BgReading last1 = bgReadings.get(sizeRecords - 3); + BgReading last2 = bgReadings.get(sizeRecords - 4); + if (last2.value > 30) { + minutes = 3 * 5; + change = now.value - last2.value; + } else if (last1.value > 30) { + minutes = 2 * 5; + change = now.value - last1.value; + } else if (last.value > 30) { + minutes = 5; + change = now.value - last.value; + } else { + change = 0; + } + //multiply by 5 to get the same unit as delta, i.e. mg/dL/5m + avg = change / minutes * 5; + + result.glucose = now.value; + result.delta = change; + result.avgdelta = avg; + } + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + return result; + } } diff --git a/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java b/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java index 755e94e6fb..e9ca5fad42 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java +++ b/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java @@ -12,6 +12,7 @@ import java.util.TimeZone; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.data.Iob; +import info.nightscout.androidaps.plugins.OpenAPSMA.IobTotal; import info.nightscout.client.data.NSProfile; @DatabaseTable(tableName = "TempBasals") @@ -52,8 +53,8 @@ public class TempBasal { public boolean isAbsolute; // true if if set as absolute value in U - public Iob iobCalc(Date time) { - Iob result = new Iob(); + public IobTotal iobCalc(Date time) { + IobTotal result = new IobTotal(); NSProfile profile = MainApp.getNSProfile(); if (profile == null) @@ -89,8 +90,16 @@ public class TempBasal { tempBolusPart.insulin = tempBolusSize; Long date = this.timeStart.getTime() + j * tempBolusSpacing * 60 * 1000; tempBolusPart.created_at = new Date(date); - Iob iob = tempBolusPart.iobCalc(time); - result.plus(iob); + + Iob aIOB = tempBolusPart.iobCalc(time, profile.getDia()); + result.basaliob += aIOB.iobContrib; + Double dia_ago = time.getTime() - profile.getDia() * 60 * 60 * 1000; + if (date > dia_ago && date <= time.getTime()) { + result.netbasalinsulin += tempBolusPart.insulin; + if (tempBolusPart.insulin > 0) { + result.hightempinsulin += tempBolusPart.insulin; + } + } } } } diff --git a/app/src/main/java/info/nightscout/androidaps/db/Treatment.java b/app/src/main/java/info/nightscout/androidaps/db/Treatment.java index 59bb255062..56ce9a8a3f 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/Treatment.java +++ b/app/src/main/java/info/nightscout/androidaps/db/Treatment.java @@ -56,24 +56,8 @@ public class Treatment { this.carbs = t.carbs; } - public Iob iobCalc(Date time) { - Iob resultNow = doIobCalc(time); - Iob resultIn5min = doIobCalc(new Date(time.getTime() + 5 * 60 * 1000)); - resultNow.activityContrib = resultNow.iobContrib - resultIn5min.iobContrib; - return resultNow; - } - - public Iob doIobCalc(Date time) { - + public Iob iobCalc(Date time, Double dia) { Iob result = new Iob(); - NSProfile profile = MainApp.getNSProfile(); - - if (profile == null) { - return result; - } - - Double dia = profile.getDia(); - Double sens = profile.getIsf(profile.secondsFromMidnight(time)); Double scaleFactor = 3.0 / dia; Double peak = 75d; @@ -98,22 +82,6 @@ public class Treatment { return result; } -/* - public Iob calcIobOpenAPS() { - IobCalc calc = new IobCalc(created_at,insulin,new Date()); - calc.setBolusDiaTimesTwo(); - Iob iob = calc.invoke(); - - return iob; - } - public Iob calcIob() { - IobCalc calc = new IobCalc(created_at,insulin,new Date()); - Iob iob = calc.invoke(); - - return iob; - } -*/ - public long getMillisecondsFromStart() { return new Date().getTime() - created_at.getTime(); } @@ -180,6 +148,4 @@ public class Treatment { log.error("DBUPDATE No receivers"); } else log.debug("DBUPDATE dbUpdate " + q.size() + " receivers " + _id + " " + data.toString()); } - - } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/APSBase.java b/app/src/main/java/info/nightscout/androidaps/plugins/APSBase.java new file mode 100644 index 0000000000..b1fd2e9510 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/APSBase.java @@ -0,0 +1,13 @@ +package info.nightscout.androidaps.plugins; + +import java.util.Date; + +/** + * Created by mike on 10.06.2016. + */ +public interface APSBase { + public APSResult getLastAPSResult(); + public Date getLastAPSRun(); + + public void run(); +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/APSResult.java b/app/src/main/java/info/nightscout/androidaps/plugins/APSResult.java new file mode 100644 index 0000000000..b5d2b42724 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/APSResult.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.plugins; + +/** + * Created by mike on 09.06.2016. + */ +public class APSResult { + public String reason; + public double rate; + public int duration; + public boolean changeRequested = false; +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendFragment.java new file mode 100644 index 0000000000..492ca5e397 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendFragment.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.plugins.LowSuspend; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.PluginBase; + +public class LowSuspendFragment extends Fragment implements PluginBase { + + @Override + public int getType() { + return PluginBase.APS; + } + + @Override + public boolean isFragmentVisible() { + return true; + } + + public static LowSuspendFragment newInstance() { + LowSuspendFragment fragment = new LowSuspendFragment(); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + registerBus(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.lowsuspend_fragment, container, false); + } + + private void registerBus() { + try { + MainApp.bus().unregister(this); + } catch (RuntimeException x) { + // Ignore + } + MainApp.bus().register(this); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendResult.java b/app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendResult.java new file mode 100644 index 0000000000..d301b735e1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/LowSuspend/LowSuspendResult.java @@ -0,0 +1,25 @@ +package info.nightscout.androidaps.plugins.LowSuspend; + +import org.json.JSONException; +import org.json.JSONObject; + +public class LowSuspendResult { + public boolean lowProjected; + public boolean low; + public String reason; + + public int percent; + + public JSONObject json() { + JSONObject json = new JSONObject(); + try { + json.put("low", low); + json.put("lowProjected", lowProjected); + json.put("reason", reason); + json.put("percent", percent); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterJS.java new file mode 100644 index 0000000000..bcc7d62593 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalAdapterJS.java @@ -0,0 +1,244 @@ +package info.nightscout.androidaps.plugins.OpenAPSMA; + +import com.eclipsesource.v8.JavaVoidCallback; +import com.eclipsesource.v8.V8; +import com.eclipsesource.v8.V8Array; +import com.eclipsesource.v8.V8Object; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import info.nightscout.androidaps.Config; +import info.nightscout.androidaps.data.Pump; +import info.nightscout.androidaps.db.DatabaseHelper; +import info.nightscout.androidaps.plugins.ScriptReader; +import info.nightscout.androidaps.plugins.Treatments.TreatmentsFragment; +import info.nightscout.client.data.NSProfile; + +public class DetermineBasalAdapterJS { + private static Logger log = LoggerFactory.getLogger(DetermineBasalAdapterJS.class); + + + private final ScriptReader mScriptReader; + V8 mV8rt; + private V8Object mProfile; + private V8Object mGlucoseStatus; + private V8Object mIobData; + private V8Object mMealData; + private V8Object mCurrentTemp; + + private final String PARAM_currentTemp = "currentTemp"; + private final String PARAM_iobData = "iobData"; + private final String PARAM_glucoseStatus = "glucose_status"; + private final String PARAM_profile = "profile"; + private final String PARAM_meal_data = "meal_data"; + + public DetermineBasalAdapterJS(ScriptReader scriptReader) throws IOException { + mV8rt = V8.createV8Runtime(); + mScriptReader = scriptReader; + + init(); + initLogCallback(); + initProcessExitCallback(); + initModuleParent(); + loadScript(); + } + + public void init() { + // Profile + mProfile = new V8Object(mV8rt); + mProfile.add("max_iob", 0); + mProfile.add("carbs_hr", 0); + mProfile.add("dia", 0); + mProfile.add("type", "current"); + mProfile.add("max_daily_basal", 0); + mProfile.add("max_basal", 0); + mProfile.add("max_bg", 0); + mProfile.add("min_bg", 0); + mProfile.add("carbratio", 0); + mProfile.add("sens", 0); + mProfile.add("current_basal", 0); + mV8rt.add(PARAM_profile, mProfile); + // Current temp + mCurrentTemp = new V8Object(mV8rt); + mCurrentTemp.add("temp", "absolute"); + mCurrentTemp.add("duration", 0); + mCurrentTemp.add("rate", 0); + mV8rt.add(PARAM_currentTemp, mCurrentTemp); + // IOB data + mIobData = new V8Object(mV8rt); + mIobData.add("iob", 0); //netIob + mIobData.add("activity", 0); //netActivity + mIobData.add("bolusiob", 0); // backward compatibility with master + mIobData.add("bolussnooze", 0); //bolusIob + mIobData.add("basaliob", 0); + mIobData.add("netbasalinsulin", 0); + mIobData.add("hightempinsulin", 0); + mV8rt.add(PARAM_iobData, mIobData); + // Glucose status + mGlucoseStatus = new V8Object(mV8rt); + mGlucoseStatus.add("delta", 0); + mGlucoseStatus.add("glucose", 0); + mGlucoseStatus.add("avgdelta", 0); + mV8rt.add(PARAM_glucoseStatus, mGlucoseStatus); + // Meal data + mMealData = new V8Object(mV8rt); + mMealData.add("carbs", 0); + mMealData.add("boluses", 0); + mV8rt.add(PARAM_meal_data, mMealData); + } + + public DetermineBasalResult invoke() { + mV8rt.executeVoidScript( + "console.error(\"determine_basal(\"+\n" + + "JSON.stringify(" + PARAM_glucoseStatus + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_currentTemp + ")+ \", \" + \n" + + "JSON.stringify(" + PARAM_iobData + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_meal_data + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_profile + ")+ \") \");"); + mV8rt.executeVoidScript( + "var rT = determine_basal(" + + PARAM_glucoseStatus + ", " + + PARAM_currentTemp + ", " + + PARAM_iobData + ", " + + PARAM_profile + ", " + + "undefined, " + + PARAM_meal_data + ", " + + "setTempBasal" + + ");"); + + + String ret = mV8rt.executeStringScript("JSON.stringify(rT);"); + if (Config.logAPSResult) + log.debug(ret); + + V8Object v8ObjectReuslt = mV8rt.getObject("rT"); + + DetermineBasalResult result = null; + try { + result = new DetermineBasalResult(v8ObjectReuslt, new JSONObject(ret)); + } catch (JSONException e) { + e.printStackTrace(); + } + return result; + } + + private void loadScript() throws IOException { + mV8rt.executeVoidScript( + readFile("OpenAPSMA/determine-basal.js"), + "OpenAPSMA/bin/oref0-determine-basal.js", + 0); + mV8rt.executeVoidScript("var determine_basal = module.exports;"); + // TODO: convert to variable too + mV8rt.executeVoidScript( + "var setTempBasal = function (rate, duration, profile, rT, offline) {" + + "rT.duration = duration;\n" + + " rT.rate = rate;" + + "return rT;" + + "};", + "setTempBasal.js", + 0 + ); + } + + private void initModuleParent() { + mV8rt.executeVoidScript("var module = {\"parent\":Boolean(1)};"); + } + + private void initProcessExitCallback() { + JavaVoidCallback callbackProccessExit = new JavaVoidCallback() { + @Override + public void invoke(V8Object arg0, V8Array parameters) { + if (parameters.length() > 0) { + Object arg1 = parameters.get(0); + log.error("ProccessExit " + arg1); + } + } + }; + mV8rt.registerJavaMethod(callbackProccessExit, "proccessExit"); + mV8rt.executeVoidScript("var process = {\"exit\": function () { proccessExit(); } };"); + } + + private void initLogCallback() { + JavaVoidCallback callbackLog = new JavaVoidCallback() { + @Override + public void invoke(V8Object arg0, V8Array parameters) { + if (parameters.length() > 0) { + Object arg1 = parameters.get(0); + if (Config.logAPSResult) + log.debug("JSLOG " + arg1); + } + } + }; + mV8rt.registerJavaMethod(callbackLog, "log"); + mV8rt.executeVoidScript("var console = {\"log\":log, \"error\":log};"); + } + + + public void setData(NSProfile profile, + double maxIob, + double maxBasal, + double minBg, + double maxBg, + Pump pump, + IobTotal iobData, + DatabaseHelper.GlucoseStatus glucoseStatus, + TreatmentsFragment.MealData mealData) { + + String units = profile.getUnits(); + + mProfile.add("max_iob", maxIob); + mProfile.add("carbs_hr", profile.getCarbAbsorbtionRate()); + mProfile.add("dia", profile.getDia()); + mProfile.add("type", "current"); + mProfile.add("max_daily_basal", profile.getMaxDailyBasal()); + mProfile.add("max_basal", maxBasal); + mProfile.add("min_bg", minBg); + mProfile.add("max_bg", maxBg); + mProfile.add("carbratio", profile.getIc(profile.secondsFromMidnight())); + mProfile.add("sens", NSProfile.toMgdl(profile.getIsf(profile.secondsFromMidnight()).doubleValue(), units)); + + mProfile.add("current_basal", pump.getBaseBasalRate()); + mCurrentTemp.add("duration", pump.getTempBasalRemainingMinutes()); + mCurrentTemp.add("rate", pump.getTempBasalAbsoluteRate()); + + mIobData.add("iob", iobData.iob); //netIob + mIobData.add("activity", iobData.activity); //netActivity + mIobData.add("bolusiob", iobData.bolussnooze); // backward compatibility with master + mIobData.add("bolussnooze", iobData.bolussnooze); //bolusIob + mIobData.add("basaliob", iobData.basaliob); + mIobData.add("netbasalinsulin", iobData.netbasalinsulin); + mIobData.add("hightempinsulin", iobData.hightempinsulin); + + mGlucoseStatus.add("glucose", glucoseStatus.glucose); + mGlucoseStatus.add("delta", glucoseStatus.delta); + mGlucoseStatus.add("avgdelta", glucoseStatus.avgdelta); + + mMealData.add("carbs", mealData.carbs); + mMealData.add("boluses", mealData.boluses); + } + + + public void release() { + mProfile.release(); + mCurrentTemp.release(); + mIobData.release(); + mMealData.release(); + mGlucoseStatus.release(); + mV8rt.release(); + } + + public String readFile(String filename) throws IOException { + byte[] bytes = mScriptReader.readFile(filename); + String string = new String(bytes, "UTF-8"); + if (string.startsWith("#!/usr/bin/env node")) { + string = string.substring(20); + } + return string; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalResult.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalResult.java new file mode 100644 index 0000000000..c45a69be8d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/DetermineBasalResult.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins.OpenAPSMA; + +import com.eclipsesource.v8.V8Object; + +import org.json.JSONObject; + +import info.nightscout.androidaps.plugins.APSResult; + +public class DetermineBasalResult extends APSResult { + + public JSONObject json = new JSONObject(); + public final double eventualBG; + public final double snoozeBG; + public final String mealAssist; + + public DetermineBasalResult(V8Object result, JSONObject j) { + json = j; + reason = result.getString("reason"); + eventualBG = result.getDouble("eventualBG"); + snoozeBG = result.getDouble("snoozeBG"); + if(result.contains("rate")) { + rate = result.getDouble("rate"); + changeRequested = true; + } else { + rate = -1; + changeRequested = false; + } + if(result.contains("duration")) { + duration = result.getInteger("duration"); + changeRequested = changeRequested & true; + } else { + duration = -1; + changeRequested = false; + } + if(result.contains("mealAssist")) { + mealAssist = result.getString("mealAssist"); + } else mealAssist = ""; + + result.release(); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/IobTotal.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/IobTotal.java new file mode 100644 index 0000000000..0b167b8a94 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/IobTotal.java @@ -0,0 +1,77 @@ +package info.nightscout.androidaps.plugins.OpenAPSMA; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; + +import info.nightscout.utils.DateUtil; + +public class IobTotal { + public Double iob; + public Double activity; + public Double bolussnooze; + public Double basaliob; + public Double netbasalinsulin; + public Double hightempinsulin; + + public Double netInsulin = 0d; // for calculations from temp basals only + public Double netRatio = 0d; // for calculations from temp basals only + + public IobTotal() { + this.iob = 0d; + this.activity = 0d; + this.bolussnooze = 0d; + this.basaliob = 0d; + this.netbasalinsulin = 0d; + this.hightempinsulin = 0d; + } + + public IobTotal plus(IobTotal other) { + iob += other.iob; + activity = other.activity; + bolussnooze = other.bolussnooze; + basaliob = other.iob; + netbasalinsulin = other.netbasalinsulin; + hightempinsulin = other.hightempinsulin; + netInsulin += other.netInsulin; + netRatio += other.netRatio; + return this; + } + + public static IobTotal combine(IobTotal bolusIOB, IobTotal basalIob) { + IobTotal result = new IobTotal(); + result.iob = bolusIOB.iob; + result.activity = bolusIOB.activity; + result.bolussnooze = bolusIOB.bolussnooze; + result.basaliob = basalIob.iob; + result.netbasalinsulin = basalIob.netbasalinsulin; + result.hightempinsulin = basalIob.hightempinsulin; + return result; + } + + public JSONObject json() { + JSONObject json = new JSONObject(); + try { + json.put("iob", iob); + json.put("activity", activity); + json.put("bolusIob", bolussnooze); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + + public JSONObject nsJson() { + JSONObject json = new JSONObject(); + try { + json.put("iob", bolussnooze); + json.put("basaliob", iob); + json.put("activity", activity); + json.put("timestamp", DateUtil.toISOString(new Date())); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAFragment.java new file mode 100644 index 0000000000..b12829c986 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSMA/OpenAPSMAFragment.java @@ -0,0 +1,209 @@ +package info.nightscout.androidaps.plugins.OpenAPSMA; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.squareup.otto.Subscribe; + +import org.json.JSONException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Date; + +import info.nightscout.androidaps.Config; +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.MainActivity; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.Iob; +import info.nightscout.androidaps.data.Pump; +import info.nightscout.androidaps.db.DatabaseHelper; +import info.nightscout.androidaps.events.EventNewBG; +import info.nightscout.androidaps.events.EventTreatmentChange; +import info.nightscout.androidaps.plugins.APSBase; +import info.nightscout.androidaps.plugins.APSResult; +import info.nightscout.androidaps.plugins.PluginBase; +import info.nightscout.androidaps.plugins.ScriptReader; +import info.nightscout.androidaps.plugins.Treatments.TreatmentsFragment; +import info.nightscout.client.data.NSProfile; +import info.nightscout.utils.DateUtil; + +public class OpenAPSMAFragment extends Fragment implements View.OnClickListener, PluginBase, APSBase { + private static Logger log = LoggerFactory.getLogger(OpenAPSMAFragment.class); + + Button run; + + Date lastAPSRun = null; + APSResult lastAPSResult = null; + + @Override + public int getType() { + return PluginBase.APS; + } + + @Override + public boolean isFragmentVisible() { + return true; + } + + @Override + public APSResult getLastAPSResult() { + return lastAPSResult; + } + + @Override + public Date getLastAPSRun() { + return lastAPSRun; + } + + public static OpenAPSMAFragment newInstance() { + OpenAPSMAFragment fragment = new OpenAPSMAFragment(); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + registerBus(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.openapsma_fragment, container, false); + + run = (Button) view.findViewById(R.id.openapsma_run); + run.setOnClickListener(this); + + return view; + } + + private void registerBus() { + try { + MainApp.bus().unregister(this); + } catch (RuntimeException x) { + // Ignore + } + MainApp.bus().register(this); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.openapsma_run: + run(); + break; + } + + } + + @Subscribe + public void onStatusEvent(final EventTreatmentChange ev) { + Activity activity = getActivity(); + if (activity != null) + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + run(); + } + }); + else + log.debug("EventTreatmentChange: Activity is null"); + } + + @Subscribe + public void onStatusEvent(final EventNewBG ev) { + Activity activity = getActivity(); + if (activity != null) + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + run(); + } + }); + else + log.debug("EventNewBG: Activity is null"); + } + + @Override + public void run() { + + // private DatermineBasalResult openAps(int glucoseValue, int delta, double deltaAvg15min, StatusEvent status, LowSuspendStatus lowSuspendStatus, IobTotal iobTotal, CarbCalc.Meal mealdata) { + DetermineBasalAdapterJS determineBasalAdapterJS = null; + try { + determineBasalAdapterJS = new DetermineBasalAdapterJS(new ScriptReader(MainApp.instance().getBaseContext())); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + + DatabaseHelper.GlucoseStatus glucoseStatus = MainApp.getDbHelper().getGlucoseStatusData(); + NSProfile profile = MainApp.getNSProfile(); + Pump pump = MainApp.getActivePump(); + + if (glucoseStatus == null) { + if (Config.logAPSResult) log.debug("No glucose data available"); + return; + } + + if (profile == null) { + if (Config.logAPSResult) log.debug("No profile available"); + return; + } + + if (pump == null) { + if (Config.logAPSResult) log.debug("No pump available"); + return; + } + + SharedPreferences SP = PreferenceManager.getDefaultSharedPreferences(MainApp.instance().getApplicationContext()); + String units = profile.getUnits(); + + String maxBgDefault = "180"; + String minBgDefault = "100"; + if (!units.equals(Constants.MGDL)) { + maxBgDefault = "10"; + minBgDefault = "5"; + } + + // TODO: objectives limits + double maxIob = Double.parseDouble(SP.getString("max_iob", "1.5").replace(",", ".")); + double maxBasal = Double.parseDouble(SP.getString("max_basal", "1").replace(",", ".")); + // TODO: min_bg, max_bg in prefs + double minBg = NSProfile.toMgdl(Double.parseDouble(SP.getString("min_bg", minBgDefault).replace(",", ".")), units); + double maxBg = NSProfile.toMgdl(Double.parseDouble(SP.getString("max_bg", maxBgDefault).replace(",", ".")), units); + + MainActivity.treatmentsFragment.updateTotalIOBIfNeeded(); + MainActivity.tempBasalsFragment.updateTotalIOBIfNeeded(); + IobTotal bolusIob = MainActivity.treatmentsFragment.lastCalculation; + IobTotal basalIob = MainActivity.tempBasalsFragment.lastCalculation; + + IobTotal iobTotal = IobTotal.combine(bolusIob, basalIob); + + TreatmentsFragment.MealData mealData = MainActivity.treatmentsFragment.getMealData(); + + determineBasalAdapterJS.setData(profile, maxIob, maxBasal, minBg, maxBg, pump, iobTotal, glucoseStatus, mealData); + DetermineBasalResult determineBasalResult = determineBasalAdapterJS.invoke(); + determineBasalAdapterJS.release(); + + try { + determineBasalResult.json.put("timestamp", DateUtil.toISOString(new Date())); + } catch (JSONException e) { + e.printStackTrace(); + } + lastAPSResult = determineBasalResult; + lastAPSRun = new Date(); + + //deviceStatus.suggested = determineBasalResult.json; + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java index 77422dfedd..7d79cc72fa 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java @@ -4,14 +4,12 @@ import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.app.ShareCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.jjoe64.graphview.GraphView; -import com.jjoe64.graphview.helper.DateAsXAxisLabelFormatter; import com.jjoe64.graphview.series.DataPoint; import com.jjoe64.graphview.series.LineGraphSeries; import com.jjoe64.graphview.series.PointsGraphSeries; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/PluginBase.java b/app/src/main/java/info/nightscout/androidaps/plugins/PluginBase.java index 307cd03476..5a9abee63d 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/PluginBase.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/PluginBase.java @@ -1,5 +1,7 @@ package info.nightscout.androidaps.plugins; +import java.util.Date; + /** * Created by mike on 09.06.2016. */ diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/ScriptReader.java b/app/src/main/java/info/nightscout/androidaps/plugins/ScriptReader.java new file mode 100644 index 0000000000..ea90cf60b9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/ScriptReader.java @@ -0,0 +1,41 @@ +package info.nightscout.androidaps.plugins; + +import android.content.Context; +import android.content.res.AssetManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ScriptReader { + + private final Context mContext; + + public ScriptReader(Context context) { + mContext = context; + } + + public byte[] readFile(String fileName) throws IOException { + + AssetManager assetManager = mContext.getAssets(); + InputStream is = assetManager.open(fileName); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int nRead; + byte[] data = new byte[16384]; + + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + + buffer.flush(); + + byte[] bytes = buffer.toByteArray(); + is.close(); + buffer.close(); + + + return bytes; + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java index b7b9fe64d2..4e77987ff5 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java @@ -35,6 +35,7 @@ import info.nightscout.androidaps.data.Iob; import info.nightscout.androidaps.db.TempBasal; import info.nightscout.androidaps.events.EventNewBG; import info.nightscout.androidaps.events.EventTempBasalChange; +import info.nightscout.androidaps.plugins.OpenAPSMA.IobTotal; import info.nightscout.androidaps.plugins.PluginBase; @@ -45,10 +46,9 @@ public class TempBasalsFragment extends Fragment implements PluginBase { LinearLayoutManager llm; TextView iobTotal; - TextView activityTotal; public long lastCalculationTimestamp = 0; - public Iob lastCalculation; + public IobTotal lastCalculation; private static DecimalFormat formatNumber0decimalplaces = new DecimalFormat("0"); private static DecimalFormat formatNumber2decimalplaces = new DecimalFormat("0.00"); @@ -107,15 +107,14 @@ public class TempBasalsFragment extends Fragment implements PluginBase { } private void updateTotalIOB() { - Iob total = new Iob(); + Date now = new Date(); + IobTotal total = new IobTotal(); for (Integer pos = 0; pos < tempBasals.size(); pos++) { TempBasal t = tempBasals.get(pos); - total.plus(t.iobCalc(new Date())); + total.plus(t.iobCalc(now)); } if (iobTotal != null) - iobTotal.setText(formatNumber2decimalplaces.format(total.iobContrib)); - if (activityTotal != null) - activityTotal.setText(formatNumber3decimalplaces.format(total.activityContrib)); + iobTotal.setText(formatNumber2decimalplaces.format(total.basaliob)); } public static class RecyclerViewAdapter extends RecyclerView.Adapter { @@ -152,9 +151,8 @@ public class TempBasalsFragment extends Fragment implements PluginBase { holder.percent.setText(formatNumber0decimalplaces.format(tempBasals.get(position).percent) + "%"); } holder.realDuration.setText(formatNumber0decimalplaces.format(tempBasals.get(position).getRealDuration()) + " min"); - Iob iob = tempBasals.get(position).iobCalc(new Date()); - holder.iob.setText(formatNumber2decimalplaces.format(iob.iobContrib) + " U"); - holder.activity.setText(formatNumber3decimalplaces.format(iob.activityContrib) + " U"); + IobTotal iob = tempBasals.get(position).iobCalc(new Date()); + holder.iob.setText(formatNumber2decimalplaces.format(iob.basaliob) + " U"); holder.netInsulin.setText(formatNumber2decimalplaces.format(iob.netInsulin) + " U"); holder.netRatio.setText(formatNumber2decimalplaces.format(iob.netRatio) + " U/h"); } @@ -179,7 +177,6 @@ public class TempBasalsFragment extends Fragment implements PluginBase { TextView netRatio; TextView netInsulin; TextView iob; - TextView activity; TempBasalsViewHolder(View itemView) { super(itemView); @@ -192,7 +189,6 @@ public class TempBasalsFragment extends Fragment implements PluginBase { netRatio = (TextView) itemView.findViewById(R.id.tempbasals_netratio); netInsulin = (TextView) itemView.findViewById(R.id.tempbasals_netinsulin); iob = (TextView) itemView.findViewById(R.id.tempbasals_iob); - activity = (TextView) itemView.findViewById(R.id.tempbasals_activity); } } } @@ -227,7 +223,6 @@ public class TempBasalsFragment extends Fragment implements PluginBase { recyclerView.setAdapter(adapter); iobTotal = (TextView) view.findViewById(R.id.tempbasals_iobtotal); - activityTotal = (TextView) view.findViewById(R.id.tempbasals_iobactivitytotal); return view; } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/NewTreatmentDialogFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/NewTreatmentDialogFragment.java index 44beab6bd2..f39577891c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/NewTreatmentDialogFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/NewTreatmentDialogFragment.java @@ -60,8 +60,8 @@ public class NewTreatmentDialogFragment extends DialogFragment implements OnClic Double maxcarbs = Double.parseDouble(SP.getString("safety_maxcarbs", "48")); - String insulinText = this.insulin.getText().toString(); - String carbsText = this.carbs.getText().toString(); + String insulinText = this.insulin.getText().toString().replace(",", "."); + String carbsText = this.carbs.getText().toString().replace(",", "."); Double insulin = Double.parseDouble(!insulinText.equals("") ? this.insulin.getText().toString() : "0"); Double carbs = Double.parseDouble(!carbsText.equals("") ? this.carbs.getText().toString() : "0"); if (insulin > maxbolus) { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/WizardDialogFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/WizardDialogFragment.java index 0e137c7a04..64cb5e3bd4 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/WizardDialogFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/Dialogs/WizardDialogFragment.java @@ -24,6 +24,7 @@ import info.nightscout.androidaps.R; import info.nightscout.androidaps.data.Iob; import info.nightscout.androidaps.db.BgReading; import info.nightscout.androidaps.db.Treatment; +import info.nightscout.androidaps.plugins.OpenAPSMA.IobTotal; import info.nightscout.androidaps.plugins.Treatments.TreatmentsFragment; import info.nightscout.client.data.NSProfile; import info.nightscout.utils.*; @@ -175,10 +176,10 @@ public class WizardDialogFragment extends DialogFragment implements OnClickListe MainActivity.treatmentsFragment.updateTotalIOBIfNeeded(); MainActivity.tempBasalsFragment.updateTotalIOBIfNeeded(); - Iob bolusIob = MainActivity.treatmentsFragment.lastCalculation; - Iob basalIob = MainActivity.tempBasalsFragment.lastCalculation; - bolusIob.plus(basalIob); - iobInsulin.setText("-" + numberFormat.format(bolusIob.iobContrib) + "U"); + IobTotal bolusIob = MainActivity.treatmentsFragment.lastCalculation; + IobTotal basalIob = MainActivity.tempBasalsFragment.lastCalculation; + Double iobTotal = bolusIob.iob + basalIob.iob; + iobInsulin.setText("-" + numberFormat.format(iobTotal) + "U"); totalInsulin.setText(""); wizardDialogDeliverButton.setVisibility(Button.GONE); @@ -194,9 +195,9 @@ public class WizardDialogFragment extends DialogFragment implements OnClickListe NSProfile profile = MainApp.instance().getNSProfile(); // Entered values - String i_bg = this.bgInput.getText().toString(); - String i_carbs = this.carbsInput.getText().toString(); - String i_correction = this.correctionInput.getText().toString(); + String i_bg = this.bgInput.getText().toString().replace("," , "."); + String i_carbs = this.carbsInput.getText().toString().replace(",", "."); + String i_correction = this.correctionInput.getText().toString().replace(",", "."); Double c_bg = 0d; try { c_bg = Double.parseDouble(i_bg.equals("") ? "0" : i_bg); } catch (Exception e) {} Double c_carbs = 0d; @@ -240,10 +241,10 @@ public class WizardDialogFragment extends DialogFragment implements OnClickListe MainActivity.treatmentsFragment.updateTotalIOBIfNeeded(); MainActivity.tempBasalsFragment.updateTotalIOBIfNeeded(); - Iob bolusIob = MainActivity.treatmentsFragment.lastCalculation; - Iob basalIob = MainActivity.tempBasalsFragment.lastCalculation; - bolusIob.plus(basalIob); - Double insulingFromIOB = iobCheckbox.isChecked() ? bolusIob.iobContrib : 0d; + IobTotal bolusIob = MainActivity.treatmentsFragment.lastCalculation; + IobTotal basalIob = MainActivity.tempBasalsFragment.lastCalculation; + Double iobTotal = bolusIob.iob + basalIob.iob; + Double insulingFromIOB = iobCheckbox.isChecked() ? iobTotal : 0d; iobInsulin.setText("-" + numberFormat.format(insulingFromIOB) + "U"); // Insulin from correction diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java index be08746349..abbc2e642b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java @@ -39,9 +39,11 @@ import info.nightscout.androidaps.data.Iob; import info.nightscout.androidaps.db.Treatment; import info.nightscout.androidaps.events.EventNewBG; import info.nightscout.androidaps.events.EventTreatmentChange; +import info.nightscout.androidaps.plugins.OpenAPSMA.IobTotal; import info.nightscout.androidaps.plugins.PluginBase; import info.nightscout.androidaps.plugins.Treatments.Dialogs.NewTreatmentDialogFragment; import info.nightscout.androidaps.Services.Intents; +import info.nightscout.client.data.NSProfile; public class TreatmentsFragment extends Fragment implements View.OnClickListener, NewTreatmentDialogFragment.Communicator, PluginBase { private static Logger log = LoggerFactory.getLogger(TreatmentsFragment.class); @@ -54,7 +56,7 @@ public class TreatmentsFragment extends Fragment implements View.OnClickListener Button refreshFromNS; public long lastCalculationTimestamp = 0; - public Iob lastCalculation; + public IobTotal lastCalculation; private static DecimalFormat formatNumber0decimalplaces = new DecimalFormat("0"); private static DecimalFormat formatNumber2decimalplaces = new DecimalFormat("0.00"); @@ -101,20 +103,61 @@ public class TreatmentsFragment extends Fragment implements View.OnClickListener } private void updateTotalIOB() { - Iob total = new Iob(); + IobTotal total = new IobTotal(); + + NSProfile profile = MainApp.getNSProfile(); + if (profile == null) { + lastCalculation = total; + return; + } + + Double dia = profile.getDia(); + + Date now = new Date(); for (Integer pos = 0; pos < treatments.size(); pos++) { Treatment t = treatments.get(pos); - total.plus(t.iobCalc(new Date())); + Iob tIOB = t.iobCalc(now, dia); + total.iob += tIOB.iobContrib; + total.activity += tIOB.activityContrib; + Iob bIOB = t.iobCalc(now, dia / 2); + total.bolussnooze += bIOB.iobContrib; } if (iobTotal != null) - iobTotal.setText(formatNumber2decimalplaces.format(total.iobContrib)); + iobTotal.setText(formatNumber2decimalplaces.format(total.iob)); if (activityTotal != null) - activityTotal.setText(formatNumber3decimalplaces.format(total.activityContrib)); + activityTotal.setText(formatNumber3decimalplaces.format(total.activity)); lastCalculationTimestamp = new Date().getTime(); lastCalculation = total; } + public class MealData { + public double boluses = 0d; + public double carbs = 0d; + } + + public MealData getMealData() { + MealData result = new MealData(); + NSProfile profile = MainApp.getNSProfile(); + if (profile == null) + return result; + + for (Treatment treatment : treatments) { + long now = new Date().getTime(); + long dia_ago = now - (new Double(profile.getDia() * 60 * 60 * 1000l)).longValue(); + long t = treatment.created_at.getTime(); + if (t > dia_ago && t <= now) { + if (treatment.carbs >= 1) { + result.carbs += treatment.carbs; + } + if (treatment.insulin >= 0.1) { + result.boluses += treatment.insulin; + } + } + } + return result; + } + public static class RecyclerViewAdapter extends RecyclerView.Adapter { List treatments; @@ -132,12 +175,15 @@ public class TreatmentsFragment extends Fragment implements View.OnClickListener @Override public void onBindViewHolder(TreatmentsViewHolder holder, int position) { + NSProfile profile = MainApp.getNSProfile(); + if (profile == null) + return; // TODO: implement locales DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, new Locale("cs", "CZ")); holder.date.setText(df.format(treatments.get(position).created_at)); holder.insulin.setText(formatNumber2decimalplaces.format(treatments.get(position).insulin) + " U"); holder.carbs.setText(formatNumber0decimalplaces.format(treatments.get(position).carbs) + " g"); - Iob iob = treatments.get(position).iobCalc(new Date()); + Iob iob = treatments.get(position).iobCalc(new Date(), profile.getDia()); holder.iob.setText(formatNumber2decimalplaces.format(iob.iobContrib) + " U"); holder.activity.setText(formatNumber3decimalplaces.format(iob.activityContrib) + " U"); } @@ -205,8 +251,8 @@ public class TreatmentsFragment extends Fragment implements View.OnClickListener activityTotal = (TextView) view.findViewById(R.id.treatments_iobactivitytotal); refreshFromNS = (Button) view.findViewById(R.id.treatments_reshreshfromnightscout); - refreshFromNS.setOnClickListener(this); + return view; } diff --git a/app/src/main/java/info/nightscout/client/data/NSProfile.java b/app/src/main/java/info/nightscout/client/data/NSProfile.java index 9965d1db4e..c85545abf3 100644 --- a/app/src/main/java/info/nightscout/client/data/NSProfile.java +++ b/app/src/main/java/info/nightscout/client/data/NSProfile.java @@ -9,6 +9,8 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import info.nightscout.androidaps.Constants; + public class NSProfile { private JSONObject json = null; private String activeProfile = null; @@ -292,4 +294,9 @@ public class NSProfile { long passed = now - c.getTimeInMillis(); return (int) (passed / 1000); } + + public static Double toMgdl(Double value, String units) { + if (units.equals(Constants.MGDL)) return value; + else return value * Constants.MMOLL_TO_MGDL; + } } diff --git a/app/src/main/res/layout/lowsuspend_fragment.xml b/app/src/main/res/layout/lowsuspend_fragment.xml new file mode 100644 index 0000000000..b145b8b546 --- /dev/null +++ b/app/src/main/res/layout/lowsuspend_fragment.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/openapsma_fragment.xml b/app/src/main/res/layout/openapsma_fragment.xml new file mode 100644 index 0000000000..f663518f55 --- /dev/null +++ b/app/src/main/res/layout/openapsma_fragment.xml @@ -0,0 +1,19 @@ + + + + +