diff --git a/app/build.gradle b/app/build.gradle index cc26a49966..afd6151263 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,7 +109,7 @@ android { defaultConfig { multiDexEnabled true versionCode 1500 - version "3.0.0.1-dev" + version "3.0.0.1-dev-a" buildConfigField "String", "VERSION", '"' + version + '"' buildConfigField "String", "BUILDVERSION", '"' + generateGitBuild() + '-' + generateDate() + '"' buildConfigField "String", "REMOTE", '"' + generateGitRemote() + '"' diff --git a/app/src/main/assets/OpenAPSSMBDynamicISF/determine-basal.js b/app/src/main/assets/OpenAPSSMBDynamicISF/determine-basal.js new file mode 100644 index 0000000000..ce13f03387 --- /dev/null +++ b/app/src/main/assets/OpenAPSSMBDynamicISF/determine-basal.js @@ -0,0 +1,1231 @@ +/* + 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 round_basal = require('../round-basal') + +// Rounds value to 'digits' decimal places +function round(value, digits) +{ + if (! digits) { digits = 0; } + var scale = Math.pow(10, digits); + return Math.round(value * scale) / scale; +} + +// we expect BG to rise or fall at the rate of BGI, +// adjusted by the rate at which BG would need to rise / +// fall to get eventualBG to target over 2 hours +function calculate_expected_delta(target_bg, eventual_bg, bgi) { + // (hours * mins_per_hour) / 5 = how many 5 minute periods in 2h = 24 + var five_min_blocks = (2 * 60) / 5; + var target_delta = target_bg - eventual_bg; + return /* expectedDelta */ round(bgi + (target_delta / five_min_blocks), 1); +} + + +function convert_bg(value, profile) +{ + if (profile.out_units === "mmol/L") + { + return round(value / 18, 1).toFixed(1); + } + else + { + return Math.round(value); + } +} + +function enable_smb( + profile, + microBolusAllowed, + meal_data, + target_bg +) { + // disable SMB when a high temptarget is set + if (! microBolusAllowed) { + console.error("SMB disabled (!microBolusAllowed)"); + return false; + } else if (! profile.allowSMB_with_high_temptarget && profile.temptargetSet && target_bg > 100) { + console.error("SMB disabled due to high temptarget of",target_bg); + return false; + } else if (meal_data.bwFound === true && profile.A52_risk_enable === false) { + console.error("SMB disabled due to Bolus Wizard activity in the last 6 hours."); + return false; + } + + // enable SMB/UAM if always-on (unless previously disabled for high temptarget) + if (profile.enableSMB_always === true) { + if (meal_data.bwFound) { + console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard"); + } else { + console.error("SMB enabled due to enableSMB_always"); + } + return true; + } + + // enable SMB/UAM (if enabled in preferences) while we have COB + if (profile.enableSMB_with_COB === true && meal_data.mealCOB) { + if (meal_data.bwCarbs) { + console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard"); + } else { + console.error("SMB enabled for COB of",meal_data.mealCOB); + } + return true; + } + + // enable SMB/UAM (if enabled in preferences) for a full 6 hours after any carb entry + // (6 hours is defined in carbWindow in lib/meal/total.js) + if (profile.enableSMB_after_carbs === true && meal_data.carbs ) { + if (meal_data.bwCarbs) { + console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard"); + } else { + console.error("SMB enabled for 6h after carb entry"); + } + return true; + } + + // enable SMB/UAM (if enabled in preferences) if a low temptarget is set + if (profile.enableSMB_with_temptarget === true && (profile.temptargetSet && target_bg < 100)) { + if (meal_data.bwFound) { + console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard"); + } else { + console.error("SMB enabled for temptarget of",convert_bg(target_bg, profile)); + } + return true; + } + + console.error("SMB disabled (no enableSMB preferences active or no condition satisfied)"); + return false; +} + +var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, microBolusAllowed, reservoir_data, currentTime, isSaveCgmSource) { + var rT = {}; //short for requestedTemp + + var deliverAt = new Date(); + if (currentTime) { + deliverAt = new Date(currentTime); + } + + if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { + rT.error ='Error: could not get current basal rate'; + return rT; + } + var profile_current_basal = round_basal(profile.current_basal, profile); + var basal = profile_current_basal; + + var systemTime = new Date(); + if (currentTime) { + systemTime = currentTime; + } + var bgTime = new Date(glucose_status.date); + var minAgo = round( (systemTime - bgTime) / 60 / 1000 ,1); + + var bg = glucose_status.glucose; + var noise = glucose_status.noise; + // 38 is an xDrip error state that usually indicates sensor failure + // all other BG values between 11 and 37 mg/dL reflect non-error-code BG values, so we should zero temp for those + if (bg <= 10 || bg === 38 || noise >= 3) { //Dexcom is in ??? mode or calibrating, or xDrip reports high noise + rT.reason = "CGM is calibrating, in ??? state, or noise is high"; + } + if (minAgo > 12 || minAgo < -5) { // Dexcom data is too old, or way in the future + rT.reason = "If current system time "+systemTime+" is correct, then BG data is too old. The last BG data was read "+minAgo+"m ago at "+bgTime; + // if BG is too old/noisy, or is changing less than 1 mg/dL/5m for 45m, cancel any high temps and shorten any long zero temps + //cherry pick from oref upstream dev cb8e94990301277fb1016c778b4e9efa55a6edbc + } else if ( bg > 60 && glucose_status.delta == 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1 && !isSaveCgmSource) { + if ( glucose_status.last_cal && glucose_status.last_cal < 3 ) { + rT.reason = "CGM was just calibrated"; + } else { + rT.reason = "Error: CGM data is unchanged for the past ~45m"; + } + } + //cherry pick from oref upstream dev cb8e94990301277fb1016c778b4e9efa55a6edbc + if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || ( bg > 60 && glucose_status.delta == 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1 ) && !isSaveCgmSource ) { + if (currenttemp.rate > basal) { // high temp is running + rT.reason += ". Replacing high temp basal of "+currenttemp.rate+" with neutral temp of "+basal; + rT.deliverAt = deliverAt; + rT.temp = 'absolute'; + rT.duration = 30; + rT.rate = basal; + return rT; + //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } else if ( currenttemp.rate === 0 && currenttemp.duration > 30 ) { //shorten long zero temps to 30m + rT.reason += ". Shortening " + currenttemp.duration + "m long zero temp to 30m. "; + rT.deliverAt = deliverAt; + rT.temp = 'absolute'; + rT.duration = 30; + rT.rate = 0; + return rT; + //return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp); + } else { //do nothing. + rT.reason += ". Temp " + currenttemp.rate + " <= current basal " + round(basal, 2) + "U/hr; doing nothing. "; + return rT; + } + } + + var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver + + // if min and max are set, then set target to their average + var target_bg; + var min_bg; + var max_bg; + if (typeof profile.min_bg !== 'undefined') { + min_bg = profile.min_bg; + } + if (typeof profile.max_bg !== 'undefined') { + max_bg = profile.max_bg; + } + 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; + } + + var sensitivityRatio; + var high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity; + var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled target (which might change) + if ( profile.half_basal_exercise_target ) { + var halfBasalTarget = profile.half_basal_exercise_target; + } else { + halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) + // 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limited to autosens_max (1.2x by default) + } + if ( high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget + || profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget ) { + // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 + // e.g.: Sensitivity ratio set to 0.8 based on temp target of 120; Adjusting basal from 1.65 to 1.35; ISF from 58.9 to 73.6 + //sensitivityRatio = 2/(2+(target_bg-normalTarget)/40); + var c = halfBasalTarget - normalTarget; + sensitivityRatio = c/(c+target_bg-normalTarget); + // limit sensitivityRatio to profile.autosens_max (1.2x by default) + sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max); + sensitivityRatio = round(sensitivityRatio,2); + console.log("Sensitivity ratio set to "+sensitivityRatio+" based on temp target of "+target_bg+"; "); + } else if (typeof autosens_data !== 'undefined' && autosens_data) { + sensitivityRatio = autosens_data.ratio; + console.log("Autosens ratio: "+sensitivityRatio+"; "); + } + if (sensitivityRatio) { + basal = profile.current_basal * sensitivityRatio; + basal = round_basal(basal, profile); + if (basal !== profile_current_basal) { + console.log("Adjusting basal from "+profile_current_basal+" to "+basal+"; "); + } else { + console.log("Basal unchanged: "+basal+"; "); + } + } + + // adjust min, max, and target BG for sensitivity, such that 50% increase in ISF raises target from 100 to 120 + if (profile.temptargetSet) { + //console.log("Temp Target set, not adjusting with autosens; "); + } else if (typeof autosens_data !== 'undefined' && autosens_data) { + if ( profile.sensitivity_raises_target && autosens_data.ratio < 1 || profile.resistance_lowers_target && autosens_data.ratio > 1 ) { + // with a target of 100, default 0.7-1.2 autosens min/max range would allow a 93-117 target range + min_bg = round((min_bg - 60) / autosens_data.ratio) + 60; + max_bg = round((max_bg - 60) / autosens_data.ratio) + 60; + var new_target_bg = round((target_bg - 60) / autosens_data.ratio) + 60; + // don't allow target_bg below 80 + new_target_bg = Math.max(80, new_target_bg); + if (target_bg === new_target_bg) { + console.log("target_bg unchanged: "+new_target_bg+"; "); + } else { + console.log("target_bg from "+target_bg+" to "+new_target_bg+"; "); + } + target_bg = new_target_bg; + } + } + + if (typeof iob_data === 'undefined' ) { + rT.error ='Error: iob_data undefined. '; + return rT; + } + + var iobArray = iob_data; + if (typeof(iob_data.length) && iob_data.length > 1) { + iob_data = iobArray[0]; + //console.error(JSON.stringify(iob_data[0])); + } + + if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' ) { + rT.error ='Error: iob_data missing some property. '; + return rT; + } + + var tick; + + if (glucose_status.delta > -0.5) { + tick = "+" + round(glucose_status.delta,0); + } else { + tick = round(glucose_status.delta,0); + } + //var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta); + var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta); + var minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta); + var maxDelta = Math.max(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta); + + var profile_sens = round(profile.sens,1) + var sens = profile.sens; + + var now = new Date().getHours(); + if (now < 1){ + now = 1;} + else { + console.error("Time now is "+now+"; "); + } + if (meal_data.TDDAIMI7){ + var tdd7 = meal_data.TDDAIMI7; + } + else{ + var tdd7 = ((basal * 12)*100)/21; + } + var tdd_pump_now = meal_data.TDDPUMP; + var tdd_pump = ( tdd_pump_now / (now / 24)); + var TDD = (tdd7 * 0.4) + (tdd_pump * 0.6); + console.error("Pump extrapolated TDD = "+tdd_pump+"; "); + //if (tdd7 > 0){ + if ( tdd_pump > tdd7 && now < 5 || now < 7 && TDD < ( 0.8 * tdd7 ) ){ + TDD = ( 0.8 * tdd7 ); + console.log("Excess or too low insulin from pump so TDD set to "+TDD+" based on 75% of TDD7; "); + rT.reason += "TDD: " +TDD+ " due to low or high tdd from pump; "; + } + + else if (tdd_pump > (1.75 * tdd7)) { + TDD = tdd7; + console.error("TDD set to TDD7 due to high pump usage reported. TDD = "+TDD+"; "); + rT.reason += "TDD set to TDD7 due to high pump usage reported. TDD = "+TDD+"; "; + } + + else if (tdd_pump < (0.33 * tdd7)){ + TDD = (tdd7 * 0.25) + (tdd_pump * 0.75); + console.error("TDD weighted to pump due to low insulin usage. TDD = "+TDD+"; "); + rT.reason += "TDD weighted to pump due to low insulin usage. TDD = "+TDD+"; "; + } + + else { + console.log("TDD 7 ="+tdd7+", TDD Pump ="+tdd_pump+" and TDD = "+TDD+";"); + rT.reason += "TDD: " +TDD+ " based on standard pump 60/tdd7 40 split; "; + } + + + var variable_sens = (277700 / (TDD * bg)); + variable_sens = round(variable_sens,1); + console.log("Current sensitivity for predictions is " +variable_sens+" based on current bg"); + + sens = variable_sens; + if ( high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget || profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget ) { + sens = sens / sensitivityRatio ; + sens = round(sens, 1); + console.log("ISF from "+variable_sens+" to "+sens+ "due to temp target; "); + } else { + sens = sens; + sens = round(sens, 1); + } + + console.error("; CR:",profile.carb_ratio); + + // compare currenttemp to iob_data.lastTemp and cancel temp if they don't match + var lastTempAge; + if (typeof iob_data.lastTemp !== 'undefined' ) { + lastTempAge = round(( new Date(systemTime).getTime() - iob_data.lastTemp.date ) / 60000); // in minutes + } else { + lastTempAge = 0; + } + //console.error("currenttemp:",currenttemp,"lastTemp:",JSON.stringify(iob_data.lastTemp),"lastTempAge:",lastTempAge,"m"); + var tempModulus = (lastTempAge + currenttemp.duration) % 30; + console.error("currenttemp:",currenttemp,"lastTempAge:",lastTempAge,"m","tempModulus:",tempModulus,"m"); + rT.temp = 'absolute'; + rT.deliverAt = deliverAt; + if ( microBolusAllowed && currenttemp && iob_data.lastTemp && currenttemp.rate !== iob_data.lastTemp.rate && lastTempAge > 10 && currenttemp.duration ) { + rT.reason = "Warning: currenttemp rate "+currenttemp.rate+" != lastTemp rate "+iob_data.lastTemp.rate+" from pumphistory; canceling temp"; + return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); + } + if ( currenttemp && iob_data.lastTemp && currenttemp.duration > 0 ) { + // TODO: fix this (lastTemp.duration is how long it has run; currenttemp.duration is time left + //if ( currenttemp.duration < iob_data.lastTemp.duration - 2) { + //rT.reason = "Warning: currenttemp duration "+currenttemp.duration+" << lastTemp duration "+round(iob_data.lastTemp.duration,1)+" from pumphistory; setting neutral temp of "+basal+"."; + //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + //} + //console.error(lastTempAge, round(iob_data.lastTemp.duration,1), round(lastTempAge - iob_data.lastTemp.duration,1)); + var lastTempEnded = lastTempAge - iob_data.lastTemp.duration + if ( lastTempEnded > 5 && lastTempAge > 10 ) { + rT.reason = "Warning: currenttemp running but lastTemp from pumphistory ended "+lastTempEnded+"m ago; canceling temp"; + //console.error(currenttemp, round(iob_data.lastTemp,1), round(lastTempAge,1)); + return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); + } + // TODO: figure out a way to do this check that doesn't fail across basal schedule boundaries + //if ( tempModulus < 25 && tempModulus > 5 ) { + //rT.reason = "Warning: currenttemp duration "+currenttemp.duration+" + lastTempAge "+lastTempAge+" isn't a multiple of 30m; setting neutral temp of "+basal+"."; + //console.error(rT.reason); + //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + //} + } + + //calculate BG impact: the amount BG "should" be rising or falling based on insulin activity alone + var bgi = round(( -iob_data.activity * sens * 5 ), 2); + // project deviations for 30 minutes + var deviation = round( 30 / 5 * ( minDelta - bgi ) ); + // don't overreact to a big negative delta: use minAvgDelta if deviation is negative + if (deviation < 0) { + deviation = round( (30 / 5) * ( minAvgDelta - bgi ) ); + // and if deviation is still negative, use long_avgdelta + if (deviation < 0) { + deviation = round( (30 / 5) * ( glucose_status.long_avgdelta - bgi ) ); + } + } + + // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity + if (iob_data.iob > 0) { + var naive_eventualBG = round( bg - (iob_data.iob * sens) ); + } else { // if IOB is negative, be more conservative and use the lower of sens, profile.sens + naive_eventualBG = round( bg - (iob_data.iob * Math.min(sens, profile.sens) ) ); + } + // and adjust it for the deviation above + var eventualBG = naive_eventualBG + deviation; + + // raise target for noisy / raw CGM data + if (glucose_status.noise >= 2) { + // increase target at least 10% (default 30%) for raw / noisy data + var noisyCGMTargetMultiplier = Math.max( 1.1, profile.noisyCGMTargetMultiplier ); + // don't allow maxRaw above 250 + var maxRaw = Math.min( 250, profile.maxRaw ); + var adjustedMinBG = round(Math.min(200, min_bg * noisyCGMTargetMultiplier )); + var adjustedTargetBG = round(Math.min(200, target_bg * noisyCGMTargetMultiplier )); + var adjustedMaxBG = round(Math.min(200, max_bg * noisyCGMTargetMultiplier )); + console.log("Raising target_bg for noisy / raw CGM data, from "+target_bg+" to "+adjustedTargetBG+"; "); + min_bg = adjustedMinBG; + target_bg = adjustedTargetBG; + max_bg = adjustedMaxBG; + // adjust target BG range if configured to bring down high BG faster + } else if ( bg > max_bg && profile.adv_target_adjustments && ! profile.temptargetSet ) { + // with target=100, as BG rises from 100 to 160, adjustedTarget drops from 100 to 80 + adjustedMinBG = round(Math.max(80, min_bg - (bg - min_bg)/3 ),0); + adjustedTargetBG =round( Math.max(80, target_bg - (bg - target_bg)/3 ),0); + adjustedMaxBG = round(Math.max(80, max_bg - (bg - max_bg)/3 ),0); + // if eventualBG, naive_eventualBG, and target_bg aren't all above adjustedMinBG, don’t use it + //console.error("naive_eventualBG:",naive_eventualBG+", eventualBG:",eventualBG); + if (eventualBG > adjustedMinBG && naive_eventualBG > adjustedMinBG && min_bg > adjustedMinBG) { + console.log("Adjusting targets for high BG: min_bg from "+min_bg+" to "+adjustedMinBG+"; "); + min_bg = adjustedMinBG; + } else { + console.log("min_bg unchanged: "+min_bg+"; "); + } + // if eventualBG, naive_eventualBG, and target_bg aren't all above adjustedTargetBG, don’t use it + if (eventualBG > adjustedTargetBG && naive_eventualBG > adjustedTargetBG && target_bg > adjustedTargetBG) { + console.log("target_bg from "+target_bg+" to "+adjustedTargetBG+"; "); + target_bg = adjustedTargetBG; + } else { + console.log("target_bg unchanged: "+target_bg+"; "); + } + // if eventualBG, naive_eventualBG, and max_bg aren't all above adjustedMaxBG, don’t use it + if (eventualBG > adjustedMaxBG && naive_eventualBG > adjustedMaxBG && max_bg > adjustedMaxBG) { + console.error("max_bg from "+max_bg+" to "+adjustedMaxBG); + max_bg = adjustedMaxBG; + } else { + console.error("max_bg unchanged: "+max_bg); + } + } + + var expectedDelta = calculate_expected_delta(target_bg, eventualBG, bgi); + if (typeof eventualBG === 'undefined' || isNaN(eventualBG)) { + rT.error ='Error: could not calculate eventualBG. '; + return rT; + } + + // min_bg of 90 -> threshold of 65, 100 -> 70 110 -> 75, and 130 -> 85 + var threshold = min_bg - 0.5*(min_bg-40); + + //console.error(reservoir_data); + + rT = { + 'temp': 'absolute' + , 'bg': bg + , 'tick': tick + , 'eventualBG': eventualBG + , 'targetBG': target_bg + , 'insulinReq': 0 + , 'reservoir' : reservoir_data // The expected reservoir volume at which to deliver the microbolus (the reservoir volume from right before the last pumphistory run) + , 'deliverAt' : deliverAt // The time at which the microbolus should be delivered + , 'sensitivityRatio' : sensitivityRatio // autosens ratio (fraction of normal basal) + , 'variable_sens' : variable_sens + }; + + // generate predicted future BGs based on IOB, COB, and current absorption rate + + var COBpredBGs = []; + var aCOBpredBGs = []; + var IOBpredBGs = []; + var UAMpredBGs = []; + var ZTpredBGs = []; + COBpredBGs.push(bg); + aCOBpredBGs.push(bg); + IOBpredBGs.push(bg); + ZTpredBGs.push(bg); + UAMpredBGs.push(bg); + + var enableSMB = enable_smb( + profile, + microBolusAllowed, + meal_data, + target_bg + ); + + // enable UAM (if enabled in preferences) + var enableUAM=(profile.enableUAM); + + + //console.error(meal_data); + // carb impact and duration are 0 unless changed below + var ci = 0; + var cid = 0; + // calculate current carb absorption rate, and how long to absorb all carbs + // CI = current carb impact on BG in mg/dL/5m + ci = round((minDelta - bgi),1); + var uci = round((minDelta - bgi),1); + // ISF (mg/dL/U) / CR (g/U) = CSF (mg/dL/g) + + // TODO: remove commented-out code for old behavior + //if (profile.temptargetSet) { + // if temptargetSet, use unadjusted profile.sens to allow activity mode sensitivityRatio to adjust CR + //var csf = profile.sens / profile.carb_ratio; + //} else { + // otherwise, use autosens-adjusted sens to counteract autosens meal insulin dosing adjustments + // so that autotuned CR is still in effect even when basals and ISF are being adjusted by autosens + //var csf = sens / profile.carb_ratio; + //} + // use autosens-adjusted sens to counteract autosens meal insulin dosing adjustments so that + // autotuned CR is still in effect even when basals and ISF are being adjusted by TT or autosens + // this avoids overdosing insulin for large meals when low temp targets are active + csf = sens / profile.carb_ratio; + console.error("profile.sens:",profile.sens,"sens:",sens,"CSF:",csf); + + var maxCarbAbsorptionRate = 30; // g/h; maximum rate to assume carbs will absorb if no CI observed + // limit Carb Impact to maxCarbAbsorptionRate * csf in mg/dL per 5m + var maxCI = round(maxCarbAbsorptionRate*csf*5/60,1) + if (ci > maxCI) { + console.error("Limiting carb impact from",ci,"to",maxCI,"mg/dL/5m (",maxCarbAbsorptionRate,"g/h )"); + ci = maxCI; + } + var remainingCATimeMin = 3; // h; duration of expected not-yet-observed carb absorption + // adjust remainingCATime (instead of CR) for autosens if sensitivityRatio defined + if (sensitivityRatio){ + remainingCATimeMin = remainingCATimeMin / sensitivityRatio; + } + // 20 g/h means that anything <= 60g will get a remainingCATimeMin, 80g will get 4h, and 120g 6h + // when actual absorption ramps up it will take over from remainingCATime + var assumedCarbAbsorptionRate = 20; // g/h; maximum rate to assume carbs will absorb if no CI observed + var remainingCATime = remainingCATimeMin; + if (meal_data.carbs) { + // if carbs * assumedCarbAbsorptionRate > remainingCATimeMin, raise it + // so <= 90g is assumed to take 3h, and 120g=4h + remainingCATimeMin = Math.max(remainingCATimeMin, meal_data.mealCOB/assumedCarbAbsorptionRate); + var lastCarbAge = round(( new Date(systemTime).getTime() - meal_data.lastCarbTime ) / 60000); + //console.error(meal_data.lastCarbTime, lastCarbAge); + + var fractionCOBAbsorbed = ( meal_data.carbs - meal_data.mealCOB ) / meal_data.carbs; + remainingCATime = remainingCATimeMin + 1.5 * lastCarbAge/60; + remainingCATime = round(remainingCATime,1); + //console.error(fractionCOBAbsorbed, remainingCATimeAdjustment, remainingCATime) + console.error("Last carbs",lastCarbAge,"minutes ago; remainingCATime:",remainingCATime,"hours;",round(fractionCOBAbsorbed*100)+"% carbs absorbed"); + } + + // calculate the number of carbs absorbed over remainingCATime hours at current CI + // CI (mg/dL/5m) * (5m)/5 (m) * 60 (min/hr) * 4 (h) / 2 (linear decay factor) = total carb impact (mg/dL) + var totalCI = Math.max(0, ci / 5 * 60 * remainingCATime / 2); + // totalCI (mg/dL) / CSF (mg/dL/g) = total carbs absorbed (g) + var totalCA = totalCI / csf; + var remainingCarbsCap = 90; // default to 90 + var remainingCarbsFraction = 1; + if (profile.remainingCarbsCap) { remainingCarbsCap = Math.min(90,profile.remainingCarbsCap); } + if (profile.remainingCarbsFraction) { remainingCarbsFraction = Math.min(1,profile.remainingCarbsFraction); } + var remainingCarbsIgnore = 1 - remainingCarbsFraction; + var remainingCarbs = Math.max(0, meal_data.mealCOB - totalCA - meal_data.carbs*remainingCarbsIgnore); + remainingCarbs = Math.min(remainingCarbsCap,remainingCarbs); + // assume remainingCarbs will absorb in a /\ shaped bilinear curve + // peaking at remainingCATime / 2 and ending at remainingCATime hours + // area of the /\ triangle is the same as a remainingCIpeak-height rectangle out to remainingCATime/2 + // remainingCIpeak (mg/dL/5m) = remainingCarbs (g) * CSF (mg/dL/g) * 5 (m/5m) * 1h/60m / (remainingCATime/2) (h) + var remainingCIpeak = remainingCarbs * csf * 5 / 60 / (remainingCATime/2); + //console.error(profile.min_5m_carbimpact,ci,totalCI,totalCA,remainingCarbs,remainingCI,remainingCATime); + + // calculate peak deviation in last hour, and slope from that to current deviation + var slopeFromMaxDeviation = round(meal_data.slopeFromMaxDeviation,2); + // calculate lowest deviation in last hour, and slope from that to current deviation + var slopeFromMinDeviation = round(meal_data.slopeFromMinDeviation,2); + // assume deviations will drop back down at least at 1/3 the rate they ramped up + var slopeFromDeviations = Math.min(slopeFromMaxDeviation,-slopeFromMinDeviation/3); + //console.error(slopeFromMaxDeviation); + + var aci = 10; + //5m data points = g * (1U/10g) * (40mg/dL/1U) / (mg/dL/5m) + // duration (in 5m data points) = COB (g) * CSF (mg/dL/g) / ci (mg/dL/5m) + // limit cid to remainingCATime hours: the reset goes to remainingCI + if (ci === 0) { + // avoid divide by zero + cid = 0; + } else { + cid = Math.min(remainingCATime*60/5/2,Math.max(0, meal_data.mealCOB * csf / ci )); + } + var acid = Math.max(0, meal_data.mealCOB * csf / aci ); + // duration (hours) = duration (5m) * 5 / 60 * 2 (to account for linear decay) + console.error("Carb Impact:",ci,"mg/dL per 5m; CI Duration:",round(cid*5/60*2,1),"hours; remaining CI (~2h peak):",round(remainingCIpeak,1),"mg/dL per 5m"); + //console.error("Accel. Carb Impact:",aci,"mg/dL per 5m; ACI Duration:",round(acid*5/60*2,1),"hours"); + var minIOBPredBG = 999; + var minCOBPredBG = 999; + var minUAMPredBG = 999; + var minGuardBG = bg; + var minCOBGuardBG = 999; + var minUAMGuardBG = 999; + var minIOBGuardBG = 999; + var minZTGuardBG = 999; + var minPredBG; + var avgPredBG; + var IOBpredBG = eventualBG; + var maxIOBPredBG = bg; + var maxCOBPredBG = bg; + var maxUAMPredBG = bg; + //var maxPredBG = bg; + var eventualPredBG = bg; + var lastIOBpredBG; + var lastCOBpredBG; + var lastUAMpredBG; + var lastZTpredBG; + var UAMduration = 0; + var remainingCItotal = 0; + var remainingCIs = []; + var predCIs = []; + try { + iobArray.forEach(function(iobTick) { + //console.error(iobTick); + var predBGI = round(( -iobTick.activity * sens * 5 ), 2); + var predZTBGI = round(( -iobTick.iobWithZeroTemp.activity * sens * 5 ), 2); + // for IOBpredBGs, predicted deviation impact drops linearly from current deviation down to zero + // over 60 minutes (data points every 5m) + var predDev = ci * ( 1 - Math.min(1,IOBpredBGs.length/(60/5)) ); + IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + predBGI + predDev; + // calculate predBGs with long zero temp without deviations + var ZTpredBG = ZTpredBGs[ZTpredBGs.length-1] + predZTBGI; + // for COBpredBGs, predicted carb impact drops linearly from current carb impact down to zero + // eventually accounting for all carbs (if they can be absorbed over DIA) + var predCI = Math.max(0, Math.max(0,ci) * ( 1 - COBpredBGs.length/Math.max(cid*2,1) ) ); + var predACI = Math.max(0, Math.max(0,aci) * ( 1 - COBpredBGs.length/Math.max(acid*2,1) ) ); + // if any carbs aren't absorbed after remainingCATime hours, assume they'll absorb in a /\ shaped + // bilinear curve peaking at remainingCIpeak at remainingCATime/2 hours (remainingCATime/2*12 * 5m) + // and ending at remainingCATime h (remainingCATime*12 * 5m intervals) + var intervals = Math.min( COBpredBGs.length, (remainingCATime*12)-COBpredBGs.length ); + var remainingCI = Math.max(0, intervals / (remainingCATime/2*12) * remainingCIpeak ); + remainingCItotal += predCI+remainingCI; + remainingCIs.push(round(remainingCI,0)); + predCIs.push(round(predCI,0)); + //console.log(round(predCI,1)+"+"+round(remainingCI,1)+" "); + COBpredBG = COBpredBGs[COBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predCI + remainingCI; + var aCOBpredBG = aCOBpredBGs[aCOBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predACI; + // for UAMpredBGs, predicted carb impact drops at slopeFromDeviations + // calculate predicted CI from UAM based on slopeFromDeviations + var predUCIslope = Math.max(0, uci + ( UAMpredBGs.length*slopeFromDeviations ) ); + // if slopeFromDeviations is too flat, predicted deviation impact drops linearly from + // current deviation down to zero over 3h (data points every 5m) + var predUCImax = Math.max(0, uci * ( 1 - UAMpredBGs.length/Math.max(3*60/5,1) ) ); + //console.error(predUCIslope, predUCImax); + // predicted CI from UAM is the lesser of CI based on deviationSlope or DIA + var predUCI = Math.min(predUCIslope, predUCImax); + if(predUCI>0) { + //console.error(UAMpredBGs.length,slopeFromDeviations, predUCI); + UAMduration=round((UAMpredBGs.length+1)*5/60,1); + } + UAMpredBG = UAMpredBGs[UAMpredBGs.length-1] + predBGI + Math.min(0, predDev) + predUCI; + //console.error(predBGI, predCI, predUCI); + // truncate all BG predictions at 4 hours + if ( IOBpredBGs.length < 48) { IOBpredBGs.push(IOBpredBG); } + if ( COBpredBGs.length < 48) { COBpredBGs.push(COBpredBG); } + if ( aCOBpredBGs.length < 48) { aCOBpredBGs.push(aCOBpredBG); } + if ( UAMpredBGs.length < 48) { UAMpredBGs.push(UAMpredBG); } + if ( ZTpredBGs.length < 48) { ZTpredBGs.push(ZTpredBG); } + // calculate minGuardBGs without a wait from COB, UAM, IOB predBGs + if ( COBpredBG < minCOBGuardBG ) { minCOBGuardBG = round(COBpredBG); } + if ( UAMpredBG < minUAMGuardBG ) { minUAMGuardBG = round(UAMpredBG); } + if ( IOBpredBG < minIOBGuardBG ) { minIOBGuardBG = round(IOBpredBG); } + if ( ZTpredBG < minZTGuardBG ) { minZTGuardBG = round(ZTpredBG); } + + // set minPredBGs starting when currently-dosed insulin activity will peak + // look ahead 60m (regardless of insulin type) so as to be less aggressive on slower insulins + var insulinPeakTime = 60; + // add 30m to allow for insulin delivery (SMBs or temps) + insulinPeakTime = 90; + var insulinPeak5m = (insulinPeakTime/60)*12; + //console.error(insulinPeakTime, insulinPeak5m, profile.insulinPeakTime, profile.curve); + + // wait 90m before setting minIOBPredBG + if ( IOBpredBGs.length > insulinPeak5m && (IOBpredBG < minIOBPredBG) ) { minIOBPredBG = round(IOBpredBG); } + if ( IOBpredBG > maxIOBPredBG ) { maxIOBPredBG = IOBpredBG; } + // wait 85-105m before setting COB and 60m for UAM minPredBGs + if ( (cid || remainingCIpeak > 0) && COBpredBGs.length > insulinPeak5m && (COBpredBG < minCOBPredBG) ) { minCOBPredBG = round(COBpredBG); } + if ( (cid || remainingCIpeak > 0) && COBpredBG > maxIOBPredBG ) { maxCOBPredBG = COBpredBG; } + if ( enableUAM && UAMpredBGs.length > 12 && (UAMpredBG < minUAMPredBG) ) { minUAMPredBG = round(UAMpredBG); } + if ( enableUAM && UAMpredBG > maxIOBPredBG ) { maxUAMPredBG = UAMpredBG; } + }); + // set eventualBG to include effect of carbs + //console.error("PredBGs:",JSON.stringify(predBGs)); + } catch (e) { + console.error("Problem with iobArray. Optional feature Advanced Meal Assist disabled"); + } + if (meal_data.mealCOB) { + console.error("predCIs (mg/dL/5m):",predCIs.join(" ")); + console.error("remainingCIs: ",remainingCIs.join(" ")); + } + rT.predBGs = {}; + IOBpredBGs.forEach(function(p, i, theArray) { + theArray[i] = round(Math.min(401,Math.max(39,p))); + }); + for (var i=IOBpredBGs.length-1; i > 12; i--) { + if (IOBpredBGs[i-1] !== IOBpredBGs[i]) { break; } + else { IOBpredBGs.pop(); } + } + rT.predBGs.IOB = IOBpredBGs; + lastIOBpredBG=round(IOBpredBGs[IOBpredBGs.length-1]); + ZTpredBGs.forEach(function(p, i, theArray) { + theArray[i] = round(Math.min(401,Math.max(39,p))); + }); + for (i=ZTpredBGs.length-1; i > 6; i--) { + // stop displaying ZTpredBGs once they're rising and above target + if (ZTpredBGs[i-1] >= ZTpredBGs[i] || ZTpredBGs[i] <= target_bg) { break; } + else { ZTpredBGs.pop(); } + } + rT.predBGs.ZT = ZTpredBGs; + lastZTpredBG=round(ZTpredBGs[ZTpredBGs.length-1]); + if (meal_data.mealCOB > 0) { + aCOBpredBGs.forEach(function(p, i, theArray) { + theArray[i] = round(Math.min(401,Math.max(39,p))); + }); + for (i=aCOBpredBGs.length-1; i > 12; i--) { + if (aCOBpredBGs[i-1] !== aCOBpredBGs[i]) { break; } + else { aCOBpredBGs.pop(); } + } + } + if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { + COBpredBGs.forEach(function(p, i, theArray) { + theArray[i] = round(Math.min(401,Math.max(39,p))); + }); + for (i=COBpredBGs.length-1; i > 12; i--) { + if (COBpredBGs[i-1] !== COBpredBGs[i]) { break; } + else { COBpredBGs.pop(); } + } + rT.predBGs.COB = COBpredBGs; + lastCOBpredBG=round(COBpredBGs[COBpredBGs.length-1]); + eventualBG = Math.max(eventualBG, round(COBpredBGs[COBpredBGs.length-1]) ); + } + if (ci > 0 || remainingCIpeak > 0) { + if (enableUAM) { + UAMpredBGs.forEach(function(p, i, theArray) { + theArray[i] = round(Math.min(401,Math.max(39,p))); + }); + for (i=UAMpredBGs.length-1; i > 12; i--) { + if (UAMpredBGs[i-1] !== UAMpredBGs[i]) { break; } + else { UAMpredBGs.pop(); } + } + rT.predBGs.UAM = UAMpredBGs; + lastUAMpredBG=round(UAMpredBGs[UAMpredBGs.length-1]); + if (UAMpredBGs[UAMpredBGs.length-1]) { + eventualBG = Math.max(eventualBG, round(UAMpredBGs[UAMpredBGs.length-1]) ); + } + } + + // set eventualBG based on COB or UAM predBGs + rT.eventualBG = eventualBG; + } + + console.error("UAM Impact:",uci,"mg/dL per 5m; UAM Duration:",UAMduration,"hours"); + + console.log("EventualBG is" +eventualBG+" ;"); + + if( glucose_status.delta >= 0 || bg > 60 && glucose_status.delta < 2 && glucose_status.delta > -2 && glucose_status.short_avgdelta > -2 && glucose_status.short_avgdelta < 2 || eventualBG > target_bg && glucose_status.delta < 0 ) { + var future_sens = ( 277700 / (TDD * bg) ); + console.log("Future state sensitivity is " +future_sens+" using current bg due to no COB & small delta or variation"); + rT.reason += "Dosing sensitivity: " +future_sens+" using current BG;"; + } + else { + var future_sens = ( 277700 / (TDD * eventualBG)); + console.log("Future state sensitivity is " +future_sens+" based on eventual bg due to -ve delta"); + rT.reason += "Dosing sensitivity: " +future_sens+" using eventual BG;"; + } + var future_sens = round(future_sens,1); + + + minIOBPredBG = Math.max(39,minIOBPredBG); + minCOBPredBG = Math.max(39,minCOBPredBG); + minUAMPredBG = Math.max(39,minUAMPredBG); + minPredBG = round(minIOBPredBG); + + var fractionCarbsLeft = meal_data.mealCOB/meal_data.carbs; + // if we have COB and UAM is enabled, average both + if ( minUAMPredBG < 999 && minCOBPredBG < 999 ) { + // weight COBpredBG vs. UAMpredBG based on how many carbs remain as COB + avgPredBG = round( (1-fractionCarbsLeft)*UAMpredBG + fractionCarbsLeft*COBpredBG ); + // if UAM is disabled, average IOB and COB + } else if ( minCOBPredBG < 999 ) { + avgPredBG = round( (IOBpredBG + COBpredBG)/2 ); + // if we have UAM but no COB, average IOB and UAM + } else if ( minUAMPredBG < 999 ) { + avgPredBG = round( (IOBpredBG + UAMpredBG)/2 ); + } else { + avgPredBG = round( IOBpredBG ); + } + // if avgPredBG is below minZTGuardBG, bring it up to that level + if ( minZTGuardBG > avgPredBG ) { + avgPredBG = minZTGuardBG; + } + + // if we have both minCOBGuardBG and minUAMGuardBG, blend according to fractionCarbsLeft + if ( (cid || remainingCIpeak > 0) ) { + if ( enableUAM ) { + minGuardBG = fractionCarbsLeft*minCOBGuardBG + (1-fractionCarbsLeft)*minUAMGuardBG; + } else { + minGuardBG = minCOBGuardBG; + } + } else if ( enableUAM ) { + minGuardBG = minUAMGuardBG; + } else { + minGuardBG = minIOBGuardBG; + } + minGuardBG = round(minGuardBG); + //console.error(minCOBGuardBG, minUAMGuardBG, minIOBGuardBG, minGuardBG); + + var minZTUAMPredBG = minUAMPredBG; + // if minZTGuardBG is below threshold, bring down any super-high minUAMPredBG by averaging + // this helps prevent UAM from giving too much insulin in case absorption falls off suddenly + if ( minZTGuardBG < threshold ) { + minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2; + // if minZTGuardBG is between threshold and target, blend in the averaging + } else if ( minZTGuardBG < target_bg ) { + // target 100, threshold 70, minZTGuardBG 85 gives 50%: (85-70) / (100-70) + var blendPct = (minZTGuardBG-threshold) / (target_bg-threshold); + var blendedMinZTGuardBG = minUAMPredBG*blendPct + minZTGuardBG*(1-blendPct); + minZTUAMPredBG = (minUAMPredBG + blendedMinZTGuardBG) / 2; + //minZTUAMPredBG = minUAMPredBG - target_bg + minZTGuardBG; + // if minUAMPredBG is below minZTGuardBG, bring minUAMPredBG up by averaging + // this allows more insulin if lastUAMPredBG is below target, but minZTGuardBG is still high + } else if ( minZTGuardBG > minUAMPredBG ) { + minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2; + } + minZTUAMPredBG = round(minZTUAMPredBG); + //console.error("minUAMPredBG:",minUAMPredBG,"minZTGuardBG:",minZTGuardBG,"minZTUAMPredBG:",minZTUAMPredBG); + // if any carbs have been entered recently + if (meal_data.carbs) { + + // if UAM is disabled, use max of minIOBPredBG, minCOBPredBG + if ( ! enableUAM && minCOBPredBG < 999 ) { + minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG)); + // if we have COB, use minCOBPredBG, or blendedMinPredBG if it's higher + } else if ( minCOBPredBG < 999 ) { + // calculate blendedMinPredBG based on how many carbs remain as COB + var blendedMinPredBG = fractionCarbsLeft*minCOBPredBG + (1-fractionCarbsLeft)*minZTUAMPredBG; + // if blendedMinPredBG > minCOBPredBG, use that instead + minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG, blendedMinPredBG)); + // if carbs have been entered, but have expired, use minUAMPredBG + } else if ( enableUAM ) { + minPredBG = minZTUAMPredBG; + } else { + minPredBG = minGuardBG; + } + // in pure UAM mode, use the higher of minIOBPredBG,minUAMPredBG + } else if ( enableUAM ) { + minPredBG = round(Math.max(minIOBPredBG,minZTUAMPredBG)); + } + + // make sure minPredBG isn't higher than avgPredBG + minPredBG = Math.min( minPredBG, avgPredBG ); + + console.log("minPredBG: "+minPredBG+" minIOBPredBG: "+minIOBPredBG+" minZTGuardBG: "+minZTGuardBG); + if (minCOBPredBG < 999) { + console.log(" minCOBPredBG: "+minCOBPredBG); + } + if (minUAMPredBG < 999) { + console.log(" minUAMPredBG: "+minUAMPredBG); + } + console.error(" avgPredBG:",avgPredBG,"COB:",meal_data.mealCOB,"/",meal_data.carbs); + // But if the COB line falls off a cliff, don't trust UAM too much: + // use maxCOBPredBG if it's been set and lower than minPredBG + if ( maxCOBPredBG > bg ) { + minPredBG = Math.min(minPredBG, maxCOBPredBG); + } + + rT.COB=meal_data.mealCOB; + rT.IOB=iob_data.iob; + rT.reason="COB: " + round(meal_data.mealCOB, 1) + ", Dev: " + convert_bg(deviation, profile) + ", BGI: " + convert_bg(bgi, profile) + ", ISF: " + convert_bg(sens, profile) + ", CR: " + round(profile.carb_ratio, 2) + ", Target: " + convert_bg(target_bg, profile) + ", minPredBG " + convert_bg(minPredBG, profile) + ", minGuardBG " + convert_bg(minGuardBG, profile) + ", IOBpredBG " + convert_bg(lastIOBpredBG, profile); + if (lastCOBpredBG > 0) { + rT.reason += ", COBpredBG " + convert_bg(lastCOBpredBG, profile); + } + if (lastUAMpredBG > 0) { + rT.reason += ", UAMpredBG " + convert_bg(lastUAMpredBG, profile) + } + rT.reason += "; "; + // use naive_eventualBG if above 40, but switch to minGuardBG if both eventualBGs hit floor of 39 + var carbsReqBG = naive_eventualBG; + if ( carbsReqBG < 40 ) { + carbsReqBG = Math.min( minGuardBG, carbsReqBG ); + } + var bgUndershoot = threshold - carbsReqBG; + // calculate how long until COB (or IOB) predBGs drop below min_bg + var minutesAboveMinBG = 240; + var minutesAboveThreshold = 240; + if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { + for (i=0; i 0.20 * bg ) { + console.error("maxDelta",convert_bg(maxDelta, profile),"> 20% of BG",convert_bg(bg, profile),"- disabling SMB"); + rT.reason += "maxDelta "+convert_bg(maxDelta, profile)+" > 20% of BG "+convert_bg(bg, profile)+": SMB disabled; "; + enableSMB = false; + } + + console.error("BG projected to remain above",convert_bg(min_bg, profile),"for",minutesAboveMinBG,"minutes"); + if ( minutesAboveThreshold < 240 || minutesAboveMinBG < 60 ) { + console.error("BG projected to remain above",convert_bg(threshold,profile),"for",minutesAboveThreshold,"minutes"); + } + // include at least minutesAboveThreshold worth of zero temps in calculating carbsReq + // always include at least 30m worth of zero temp (carbs to 80, low temp up to target) + var zeroTempDuration = minutesAboveThreshold; + // BG undershoot, minus effect of zero temps until hitting min_bg, converted to grams, minus COB + var zeroTempEffect = profile.current_basal*sens*zeroTempDuration/60; + // don't count the last 25% of COB against carbsReq + var COBforCarbsReq = Math.max(0, meal_data.mealCOB - 0.25*meal_data.carbs); + var carbsReq = (bgUndershoot - zeroTempEffect) / csf - COBforCarbsReq; + zeroTempEffect = round(zeroTempEffect); + carbsReq = round(carbsReq); + console.error("naive_eventualBG:",naive_eventualBG,"bgUndershoot:",bgUndershoot,"zeroTempDuration:",zeroTempDuration,"zeroTempEffect:",zeroTempEffect,"carbsReq:",carbsReq); + if ( carbsReq >= profile.carbsReqThreshold && minutesAboveThreshold <= 45 ) { + rT.carbsReq = carbsReq; + rT.carbsReqWithin = minutesAboveThreshold; + rT.reason += carbsReq + " add'l carbs req w/in " + minutesAboveThreshold + "m; "; + } + + // don't low glucose suspend if IOB is already super negative and BG is rising faster than predicted + if (bg < threshold && iob_data.iob < -profile.current_basal*20/60 && minDelta > 0 && minDelta > expectedDelta) { + rT.reason += "IOB "+iob_data.iob+" < " + round(-profile.current_basal*20/60,2); + rT.reason += " and minDelta " + convert_bg(minDelta, profile) + " > " + "expectedDelta " + convert_bg(expectedDelta, profile) + "; "; + // predictive low glucose suspend mode: BG is / is projected to be < threshold + } else if ( bg < threshold || minGuardBG < threshold ) { + rT.reason += "minGuardBG " + convert_bg(minGuardBG, profile) + "<" + convert_bg(threshold, profile); + bgUndershoot = target_bg - minGuardBG; + var worstCaseInsulinReq = bgUndershoot / sens; + var durationReq = round(60*worstCaseInsulinReq / profile.current_basal); + durationReq = round(durationReq/30)*30; + // always set a 30-120m zero temp (oref0-pump-loop will let any longer SMB zero temp run) + durationReq = Math.min(120,Math.max(30,durationReq)); + return tempBasalFunctions.setTempBasal(0, durationReq, profile, rT, currenttemp); + } + + // if not in LGS mode, cancel temps before the top of the hour to reduce beeping/vibration + // console.error(profile.skip_neutral_temps, rT.deliverAt.getMinutes()); + if ( profile.skip_neutral_temps && rT.deliverAt.getMinutes() >= 55 ) { + rT.reason += "; Canceling temp at " + rT.deliverAt.getMinutes() + "m past the hour. "; + return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); + } + + if (eventualBG < min_bg) { // if eventual BG is below target: + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " < " + convert_bg(min_bg, profile); + // if 5m or 30m avg BG is rising faster than expected delta + if ( minDelta > expectedDelta && minDelta > 0 && !carbsReq ) { + // if naive_eventualBG < 40, set a 30m zero temp (oref0-pump-loop will let any longer SMB zero temp run) + if (naive_eventualBG < 40) { + rT.reason += ", naive_eventualBG < 40. "; + return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp); + } + if (glucose_status.delta > minDelta) { + rT.reason += ", but Delta " + convert_bg(tick, profile) + " > expectedDelta " + convert_bg(expectedDelta, profile); + } else { + rT.reason += ", but Min. Delta " + minDelta.toFixed(2) + " > Exp. Delta " + convert_bg(expectedDelta, profile); + } + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + round(basal, 2) + "U/hr. "; + return rT; + } else { + rT.reason += "; setting current basal of " + round(basal, 2) + " as temp. "; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } + + + + // calculate 30m low-temp required to get projected BG up to target + // multiply by 2 to low-temp faster for increased hypo safety + + var insulinReq = 2 * Math.min(0, (eventualBG - target_bg) / future_sens); + insulinReq = round( insulinReq , 2); + // calculate naiveInsulinReq based on naive_eventualBG + var naiveInsulinReq = Math.min(0, (naive_eventualBG - target_bg) / sens); + naiveInsulinReq = round( naiveInsulinReq , 2); + if (minDelta < 0 && minDelta > expectedDelta) { + // if we're barely falling, newinsulinReq should be barely negative + var newinsulinReq = round(( insulinReq * (minDelta / expectedDelta) ), 2); + //console.error("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq); + insulinReq = newinsulinReq; + } + // rate required to deliver insulinReq less insulin over 30m: + var rate = basal + (2 * insulinReq); + rate = round_basal(rate, profile); + + // if required temp < existing temp basal + var insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60; + // if current temp would deliver a lot (30% of basal) less than the required insulin, + // by both normal and naive calculations, then raise the rate + var minInsulinReq = Math.min(insulinReq,naiveInsulinReq); + if (insulinScheduled < minInsulinReq - basal*0.3) { + rT.reason += ", "+currenttemp.duration + "m@" + (currenttemp.rate).toFixed(2) + " is a lot less than needed. "; + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } + if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate >= currenttemp.rate * 0.8)) { + rT.reason += ", temp " + currenttemp.rate + " ~< req " + rate + "U/hr. "; + return rT; + } else { + // calculate a long enough zero temp to eventually correct back up to target + if ( rate <=0 ) { + bgUndershoot = target_bg - naive_eventualBG; + worstCaseInsulinReq = bgUndershoot / sens; + durationReq = round(60*worstCaseInsulinReq / profile.current_basal); + if (durationReq < 0) { + durationReq = 0; + // don't set a temp longer than 120 minutes + } else { + durationReq = round(durationReq/30)*30; + durationReq = Math.min(120,Math.max(0,durationReq)); + } + //console.error(durationReq); + if (durationReq > 0) { + rT.reason += ", setting " + durationReq + "m zero temp. "; + return tempBasalFunctions.setTempBasal(rate, durationReq, profile, rT, currenttemp); + } + } else { + rT.reason += ", setting " + rate + "U/hr. "; + } + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } + } + + // if eventual BG is above min but BG is falling faster than expected Delta + if (minDelta < expectedDelta) { + // if in SMB mode, don't cancel SMB zero temp + if (! (microBolusAllowed && enableSMB)) { + if (glucose_status.delta < minDelta) { + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Delta " + convert_bg(tick, profile) + " < Exp. Delta " + convert_bg(expectedDelta, profile); + } else { + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Min. Delta " + minDelta.toFixed(2) + " < Exp. Delta " + convert_bg(expectedDelta, profile); + } + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + round(basal, 2) + "U/hr. "; + return rT; + } else { + rT.reason += "; setting current basal of " + round(basal, 2) + " as temp. "; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } + } + // eventualBG or minPredBG is below max_bg + if (Math.min(eventualBG,minPredBG) < max_bg) { + // if in SMB mode, don't cancel SMB zero temp + if (! (microBolusAllowed && enableSMB )) { + rT.reason += convert_bg(eventualBG, profile)+"-"+convert_bg(minPredBG, profile)+" in range: no temp required"; + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + round(basal, 2) + "U/hr. "; + return rT; + } else { + rT.reason += "; setting current basal of " + round(basal, 2) + " as temp. "; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } + } + + // eventual BG is at/above target + // if iob is over max, just cancel any temps + if ( eventualBG >= max_bg ) { + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " >= " + convert_bg(max_bg, profile) + ", "; + } + if (iob_data.iob > max_iob) { + rT.reason += "IOB " + round(iob_data.iob,2) + " > max_iob " + max_iob; + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + round(basal, 2) + "U/hr. "; + return rT; + } else { + rT.reason += "; setting current basal of " + round(basal, 2) + " as temp. "; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } else { // otherwise, calculate 30m high-temp required to get projected BG down to target + + // insulinReq is the additional insulin required to get minPredBG down to target_bg + //console.error(minPredBG,eventualBG); + insulinReq = round( (Math.min(minPredBG,eventualBG) - target_bg) / future_sens, 2); + // if that would put us over max_iob, then reduce accordingly + if (insulinReq > max_iob-iob_data.iob) { + rT.reason += "max_iob " + max_iob + ", "; + insulinReq = max_iob-iob_data.iob; + } + + // rate required to deliver insulinReq more insulin over 30m: + rate = basal + (2 * insulinReq); + rate = round_basal(rate, profile); + insulinReq = round(insulinReq,3); + rT.insulinReq = insulinReq; + //console.error(iob_data.lastBolusTime); + // minutes since last bolus + var lastBolusAge = round(( new Date(systemTime).getTime() - iob_data.lastBolusTime ) / 60000,1); + //console.error(lastBolusAge); + //console.error(profile.temptargetSet, target_bg, rT.COB); + // only allow microboluses with COB or low temp targets, or within DIA hours of a bolus + if (microBolusAllowed && enableSMB && bg > threshold) { + // never bolus more than maxSMBBasalMinutes worth of basal + var mealInsulinReq = round( meal_data.mealCOB / profile.carb_ratio ,3); + if (typeof profile.maxSMBBasalMinutes === 'undefined' ) { + var maxBolus = round( profile.current_basal * 30 / 60 ,1); + console.error("profile.maxSMBBasalMinutes undefined: defaulting to 30m"); + // if IOB covers more than COB, limit maxBolus to 30m of basal + } else if ( iob_data.iob > mealInsulinReq && iob_data.iob > 0 ) { + console.error("IOB",iob_data.iob,"> COB",meal_data.mealCOB+"; mealInsulinReq =",mealInsulinReq); + if (profile.maxUAMSMBBasalMinutes) { + console.error("profile.maxUAMSMBBasalMinutes:",profile.maxUAMSMBBasalMinutes,"profile.current_basal:",profile.current_basal); + maxBolus = round( profile.current_basal * profile.maxUAMSMBBasalMinutes / 60 ,1); + } else { + console.error("profile.maxUAMSMBBasalMinutes undefined: defaulting to 30m"); + maxBolus = round( profile.current_basal * 30 / 60 ,1); + } + } else { + console.error("profile.maxSMBBasalMinutes:",profile.maxSMBBasalMinutes,"profile.current_basal:",profile.current_basal); + maxBolus = round( profile.current_basal * profile.maxSMBBasalMinutes / 60 ,1); + } + // bolus 1/2 the insulinReq, up to maxBolus, rounding down to nearest bolus increment + var roundSMBTo = 1 / profile.bolus_increment; + var microBolus = Math.floor(Math.min(insulinReq/2,maxBolus)*roundSMBTo)/roundSMBTo; + // calculate a long enough zero temp to eventually correct back up to target + var smbTarget = target_bg; + worstCaseInsulinReq = (smbTarget - (naive_eventualBG + minIOBPredBG)/2 ) / sens; + durationReq = round(60*worstCaseInsulinReq / profile.current_basal); + + // if insulinReq > 0 but not enough for a microBolus, don't set an SMB zero temp + if (insulinReq > 0 && microBolus < profile.bolus_increment) { + durationReq = 0; + } + + var smbLowTempReq = 0; + if (durationReq <= 0) { + durationReq = 0; + // don't set an SMB zero temp longer than 60 minutes + } else if (durationReq >= 30) { + durationReq = round(durationReq/30)*30; + durationReq = Math.min(60,Math.max(0,durationReq)); + } else { + // if SMB durationReq is less than 30m, set a nonzero low temp + smbLowTempReq = round( basal * durationReq/30 ,2); + durationReq = 30; + } + rT.reason += " insulinReq " + insulinReq; + if (microBolus >= maxBolus) { + rT.reason += "; maxBolus " + maxBolus; + } + if (durationReq > 0) { + rT.reason += "; setting " + durationReq + "m low temp of " + smbLowTempReq + "U/h"; + } + rT.reason += ". "; + + //allow SMBs every 3 minutes by default + var SMBInterval = 3; + if (profile.SMBInterval) { + // allow SMBIntervals between 1 and 10 minutes + SMBInterval = Math.min(10,Math.max(1,profile.SMBInterval)); + } + var nextBolusMins = round(SMBInterval-lastBolusAge,0); + var nextBolusSeconds = round((SMBInterval - lastBolusAge) * 60, 0) % 60; + //console.error(naive_eventualBG, insulinReq, worstCaseInsulinReq, durationReq); + console.error("naive_eventualBG",naive_eventualBG+",",durationReq+"m "+smbLowTempReq+"U/h temp needed; last bolus",lastBolusAge+"m ago; maxBolus: "+maxBolus); + if (lastBolusAge > SMBInterval) { + if (microBolus > 0) { + rT.units = microBolus; + rT.reason += "Microbolusing " + microBolus + "U. "; + } + } else { + rT.reason += "Waiting " + nextBolusMins + "m " + nextBolusSeconds + "s to microbolus again. "; + } + //rT.reason += ". "; + + // if no zero temp is required, don't return yet; allow later code to set a high temp + if (durationReq > 0) { + rT.rate = smbLowTempReq; + rT.duration = durationReq; + return rT; + } + + } + + var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile); + + if (rate > maxSafeBasal) { + rT.reason += "adj. req. rate: "+round(rate, 2)+" to maxSafeBasal: "+maxSafeBasal+", "; + rate = round_basal(maxSafeBasal, profile); + } + + insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60; + if (insulinScheduled >= insulinReq * 2) { // if current temp would deliver >2x more than the required insulin, lower the rate + rT.reason += currenttemp.duration + "m@" + (currenttemp.rate).toFixed(2) + " > 2 * insulinReq. Setting temp basal of " + rate + "U/hr. "; + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } + + if (typeof currenttemp.duration === 'undefined' || currenttemp.duration === 0) { // no temp is set + rT.reason += "no temp, setting " + rate + "U/hr. "; + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } + + if (currenttemp.duration > 5 && (round_basal(rate, profile) <= round_basal(currenttemp.rate, profile))) { // 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 tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); + } + +}; + +module.exports = determine_basal; diff --git a/app/src/main/java/info/nightscout/androidaps/activities/ProfileHelperActivity.kt b/app/src/main/java/info/nightscout/androidaps/activities/ProfileHelperActivity.kt index 1f45202ab0..25fc86cf2e 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/ProfileHelperActivity.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/ProfileHelperActivity.kt @@ -239,6 +239,10 @@ class ProfileHelperActivity : NoSplashAppCompatActivity() { } ToastUtils.showToastInUiThread(this, R.string.invalidinput) } + binding.age.editText?.id?.let { binding.ageLabel.labelFor = it } + binding.tdd.editText?.id?.let { binding.tddLabel.labelFor = it } + binding.weight.editText?.id?.let { binding.weightLabel.labelFor = it } + binding.basalpctfromtdd.editText?.id?.let { binding.basalpctfromtddLabel.labelFor = it } switchTab(0, typeSelected[0], false) } diff --git a/app/src/main/java/info/nightscout/androidaps/di/APSModule.kt b/app/src/main/java/info/nightscout/androidaps/di/APSModule.kt index 235ac4065d..54036aa7d3 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/APSModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/APSModule.kt @@ -7,6 +7,7 @@ import info.nightscout.androidaps.plugins.aps.openAPSAMA.DetermineBasalAdapterAM import info.nightscout.androidaps.plugins.aps.openAPSAMA.DetermineBasalResultAMA import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalAdapterSMBJS import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalResultSMB +import info.nightscout.androidaps.plugins.aps.openAPSSMBDynamicISF.DetermineBasalAdapterSMBDynamicISFJS import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobOref1Thread import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobThread @@ -19,6 +20,7 @@ abstract class APSModule { @ContributesAndroidInjector abstract fun determineBasalResultAMAInjector(): DetermineBasalResultAMA @ContributesAndroidInjector abstract fun determineBasalAdapterAMAJSInjector(): DetermineBasalAdapterAMAJS @ContributesAndroidInjector abstract fun determineBasalAdapterSMBJSInjector(): DetermineBasalAdapterSMBJS + @ContributesAndroidInjector abstract fun determineBasalAdapterSMBAutoISFJSInjector(): DetermineBasalAdapterSMBDynamicISFJS @ContributesAndroidInjector abstract fun iobCobThreadInjector(): IobCobThread @ContributesAndroidInjector abstract fun iobCobOref1ThreadInjector(): IobCobOref1Thread } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt b/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt index 1942773a03..393eb4ba04 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt @@ -14,6 +14,7 @@ import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansUploader import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin import info.nightscout.androidaps.plugins.aps.openAPSAMA.OpenAPSAMAPlugin import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin +import info.nightscout.androidaps.plugins.aps.openAPSSMBDynamicISF.OpenAPSSMBDynamicISFPlugin import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin import info.nightscout.androidaps.plugins.constraints.bgQualityCheck.BgQualityCheckPlugin import info.nightscout.androidaps.plugins.constraints.dstHelper.DstHelperPlugin @@ -212,6 +213,12 @@ abstract class PluginsModule { @IntKey(220) abstract fun bindOpenAPSSMBPlugin(plugin: OpenAPSSMBPlugin): PluginBase + @Binds + @APS + @IntoMap + @IntKey(222) + abstract fun bindOpenAPSSMBAutoISFPlugin(plugin: OpenAPSSMBDynamicISFPlugin): PluginBase + @Binds @AllConfigs @IntoMap diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/CalibrationDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/CalibrationDialog.kt index 35f387144c..bd5daaf965 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/CalibrationDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/CalibrationDialog.kt @@ -64,6 +64,7 @@ class CalibrationDialog : DialogFragmentWithDate() { binding.bg.setParams(savedInstanceState?.getDouble("bg") ?: bg, 36.0, 500.0, 1.0, DecimalFormat("0"), false, binding.okcancel.ok) binding.units.text = if (units == GlucoseUnit.MMOL) rh.gs(R.string.mmol) else rh.gs(R.string.mgdl) + binding.bg.editText?.id?.let { binding.bgLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/CarbsDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/CarbsDialog.kt index 6f57680102..e6635ac1fb 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/CarbsDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/CarbsDialog.kt @@ -137,32 +137,39 @@ class CarbsDialog : DialogFragmentWithDate() { savedInstanceState?.getDouble("carbs") ?: 0.0, 0.0, maxCarbs, 1.0, DecimalFormat("0"), false, binding.okcancel.ok, textWatcher ) - - binding.plus1.text = toSignedString(sp.getInt(R.string.key_carbs_button_increment_1, FAV1_DEFAULT)) + val plus1text = toSignedString(sp.getInt(R.string.key_carbs_button_increment_1, FAV1_DEFAULT)) + binding.plus1.text = plus1text + binding.plus1.contentDescription = rh.gs(R.string.treatments_wizard_carbs_label) + " " + plus1text binding.plus1.setOnClickListener { binding.carbs.value = max( 0.0, binding.carbs.value + sp.getInt(R.string.key_carbs_button_increment_1, FAV1_DEFAULT) ) validateInputs() + binding.carbs.announceValue() } - binding.plus2.text = toSignedString(sp.getInt(R.string.key_carbs_button_increment_2, FAV2_DEFAULT)) + val plus2text = toSignedString(sp.getInt(R.string.key_carbs_button_increment_2, FAV2_DEFAULT)) + binding.plus2.text = plus2text + binding.plus2.contentDescription = rh.gs(R.string.treatments_wizard_carbs_label) + " " + plus2text binding.plus2.setOnClickListener { binding.carbs.value = max( 0.0, binding.carbs.value + sp.getInt(R.string.key_carbs_button_increment_2, FAV2_DEFAULT) ) validateInputs() + binding.carbs.announceValue() } - - binding.plus3.text = toSignedString(sp.getInt(R.string.key_carbs_button_increment_3, FAV3_DEFAULT)) + val plus3text = toSignedString(sp.getInt(R.string.key_carbs_button_increment_3, FAV3_DEFAULT)) + binding.plus3.text = plus3text + binding.plus2.contentDescription = rh.gs(R.string.treatments_wizard_carbs_label) + " " + plus3text binding.plus3.setOnClickListener { binding.carbs.value = max( 0.0, binding.carbs.value + sp.getInt(R.string.key_carbs_button_increment_3, FAV3_DEFAULT) ) validateInputs() + binding.carbs.announceValue() } setOnValueChangedListener { eventTime: Long -> @@ -188,6 +195,9 @@ class CarbsDialog : DialogFragmentWithDate() { binding.hypoTt.isChecked = false binding.activityTt.isChecked = false } + binding.duration.editText?.id?.let { binding.durationLabel.labelFor = it } + binding.time.editText?.id?.let { binding.timeLabel.labelFor = it } + binding.carbs.editText?.id?.let { binding.carbsLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/CareDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/CareDialog.kt index ecab62f458..94b9cf1f38 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/CareDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/CareDialog.kt @@ -165,6 +165,8 @@ class CareDialog : DialogFragmentWithDate() { ?: 0.0, 0.0, Constants.MAX_PROFILE_SWITCH_DURATION, 10.0, DecimalFormat("0"), false, binding.okcancel.ok) if (options == EventType.NOTE || options == EventType.QUESTION || options == EventType.ANNOUNCEMENT || options == EventType.EXERCISE) binding.notesLayout.root.visibility = View.VISIBLE // independent to preferences + binding.bg.editText?.id?.let { binding.bgLabel.labelFor = it } + binding.duration.editText?.id?.let { binding.durationLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/ExtendedBolusDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/ExtendedBolusDialog.kt index 4c86819465..7205a36939 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/ExtendedBolusDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/ExtendedBolusDialog.kt @@ -70,6 +70,8 @@ class ExtendedBolusDialog : DialogFragmentWithDate() { val extendedMaxDuration = pumpDescription.extendedBolusMaxDuration binding.duration.setParams(savedInstanceState?.getDouble("duration") ?: extendedDurationStep, extendedDurationStep, extendedMaxDuration, extendedDurationStep, DecimalFormat("0"), false, binding.okcancel.ok) + binding.insulin.editText?.id?.let { binding.insulinLabel.labelFor = it } + binding.duration.editText?.id?.let { binding.durationLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/FillDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/FillDialog.kt index fd8920fc07..7f94aab493 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/FillDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/FillDialog.kt @@ -96,7 +96,7 @@ class FillDialog : DialogFragmentWithDate() { } else { binding.fillPresetButton3.visibility = View.GONE } - + binding.fillInsulinamount.editText?.id?.let { binding.fillLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/InsulinDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/InsulinDialog.kt index 9cbb2426f8..b785861b5b 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/InsulinDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/InsulinDialog.kt @@ -116,29 +116,40 @@ class InsulinDialog : DialogFragmentWithDate() { binding.amount.setParams(savedInstanceState?.getDouble("amount") ?: 0.0, 0.0, maxInsulin, activePlugin.activePump.pumpDescription.bolusStep, DecimalFormatter.pumpSupportedBolusFormat(activePlugin.activePump), false, binding.okcancel.ok, textWatcher) - binding.plus05.text = sp.getDouble(rh.gs(R.string.key_insulin_button_increment_1), PLUS1_DEFAULT).toSignedString(activePlugin.activePump) + val plus05Text = sp.getDouble(rh.gs(R.string.key_insulin_button_increment_1), PLUS1_DEFAULT).toSignedString(activePlugin.activePump) + binding.plus05.text = plus05Text + binding.plus05.contentDescription = rh.gs(R.string.overview_insulin_label) + " " + plus05Text binding.plus05.setOnClickListener { binding.amount.value = max(0.0, binding.amount.value + sp.getDouble(rh.gs(R.string.key_insulin_button_increment_1), PLUS1_DEFAULT)) validateInputs() + binding.amount.announceValue() } - binding.plus10.text = sp.getDouble(rh.gs(R.string.key_insulin_button_increment_2), PLUS2_DEFAULT).toSignedString(activePlugin.activePump) + val plus10Text = sp.getDouble(rh.gs(R.string.key_insulin_button_increment_2), PLUS2_DEFAULT).toSignedString(activePlugin.activePump) + binding.plus10.text = plus10Text + binding.plus10.contentDescription = rh.gs(R.string.overview_insulin_label) + " " + plus10Text binding.plus10.setOnClickListener { binding.amount.value = max(0.0, binding.amount.value + sp.getDouble(rh.gs(R.string.key_insulin_button_increment_2), PLUS2_DEFAULT)) validateInputs() + binding.amount.announceValue() } - binding.plus20.text = sp.getDouble(rh.gs(R.string.key_insulin_button_increment_3), PLUS3_DEFAULT).toSignedString(activePlugin.activePump) + val plus20Text = sp.getDouble(rh.gs(R.string.key_insulin_button_increment_3), PLUS3_DEFAULT).toSignedString(activePlugin.activePump) + binding.plus20.text = plus20Text + binding.plus20.contentDescription = rh.gs(R.string.overview_insulin_label) + " " + plus20Text binding.plus20.setOnClickListener { binding.amount.value = max(0.0, binding.amount.value + sp.getDouble(rh.gs(R.string.key_insulin_button_increment_3), PLUS3_DEFAULT)) validateInputs() + binding.amount.announceValue() } binding.timeLayout.visibility = View.GONE binding.recordOnly.setOnCheckedChangeListener { _, isChecked: Boolean -> binding.timeLayout.visibility = isChecked.toVisibility() } + binding.amount.editText?.id?.let { binding.insulinLabel.labelFor = it } + binding.time.editText?.id?.let { binding.timeLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/ProfileSwitchDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/ProfileSwitchDialog.kt index 306aee9b56..0bed01e852 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/ProfileSwitchDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/ProfileSwitchDialog.kt @@ -148,6 +148,9 @@ class ProfileSwitchDialog : DialogFragmentWithDate() { } } binding.ttLayout.visibility = View.GONE + binding.duration.editText?.id?.let { binding.durationLabel.labelFor = it } + binding.percentage.editText?.id?.let { binding.percentageLabel.labelFor = it } + binding.timeshift.editText?.id?.let { binding.timeshiftLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/TempBasalDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/TempBasalDialog.kt index 65ee820eeb..d458055b9d 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/TempBasalDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/TempBasalDialog.kt @@ -86,6 +86,9 @@ class TempBasalDialog : DialogFragmentWithDate() { binding.percentLayout.visibility = View.GONE binding.absoluteLayout.visibility = View.VISIBLE } + binding.basalPercentInput.editText?.id?.let { binding.basalPercentLabel.labelFor = it } + binding.basalAbsoluteInput.editText?.id?.let { binding.basalAbsoluteLabel.labelFor = it } + binding.duration.editText?.id?.let { binding.durationLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/TempTargetDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/TempTargetDialog.kt index 2374c48c90..7e449950e2 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/TempTargetDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/TempTargetDialog.kt @@ -120,6 +120,8 @@ class TempTargetDialog : DialogFragmentWithDate() { longClick(it) return@setOnLongClickListener true } + binding.duration.editText?.id?.let { binding.durationLabel.labelFor = it } + binding.temptarget.editText?.id?.let { binding.temptargetLabel.labelFor = it } } } diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/TreatmentDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/TreatmentDialog.kt index 29e6290571..46b97a3f0a 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/TreatmentDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/TreatmentDialog.kt @@ -106,6 +106,8 @@ class TreatmentDialog : DialogFragmentWithDate() { binding.insulin.setParams(savedInstanceState?.getDouble("insulin") ?: 0.0, 0.0, maxInsulin, pumpDescription.bolusStep, DecimalFormatter.pumpSupportedBolusFormat(activePlugin.activePump), false, binding.okcancel.ok, textWatcher) binding.recordOnlyLayout.visibility = View.GONE + binding.insulin.editText?.id?.let { binding.insulinLabel.labelFor = it } + binding.carbs.editText?.id?.let { binding.carbsLabel.labelFor = it } } override fun onDestroyView() { diff --git a/app/src/main/java/info/nightscout/androidaps/dialogs/WizardInfoDialog.kt b/app/src/main/java/info/nightscout/androidaps/dialogs/WizardInfoDialog.kt index da6094487e..37a653f5e5 100644 --- a/app/src/main/java/info/nightscout/androidaps/dialogs/WizardInfoDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/dialogs/WizardInfoDialog.kt @@ -70,7 +70,7 @@ class WizardInfoDialog : DaggerDialogFragment() { val units = profileFunction.getUnits() val bgString = Profile.toUnitsString(data.glucoseValue, data.glucoseValue * Constants.MGDL_TO_MMOLL, units) val isf = Profile.toUnits(data.isf, data.isf * Constants.MGDL_TO_MMOLL, units) - val trend = Profile.toUnitsString(data.glucoseTrend * 3, data.glucoseTrend * 3 * Constants.MGDL_TO_MMOLL, profileFunction.getUnits()) + val trend = Profile.toUnitsString(data.glucoseTrend * 3, data.glucoseTrend * 3 * Constants.MGDL_TO_MMOLL, units) // BG binding.bg.text = rh.gs(R.string.format_bg_isf, bgString, isf) binding.bgInsulin.text = rh.gs(R.string.formatinsulinunits, data.glucoseInsulin) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt index 72196a9914..184b2b2bd3 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt @@ -7,6 +7,7 @@ import info.nightscout.androidaps.data.MealData import info.nightscout.androidaps.extensions.convertedToAbsolute import info.nightscout.androidaps.extensions.getPassedDurationToTimeInMinutes import info.nightscout.androidaps.extensions.plannedRemainingMinutes +import info.nightscout.androidaps.interfaces.DetermineBasalAdapterInterface import info.nightscout.androidaps.interfaces.GlucoseUnit import info.nightscout.androidaps.interfaces.IobCobCalculator import info.nightscout.androidaps.interfaces.Profile @@ -14,6 +15,7 @@ import info.nightscout.androidaps.interfaces.ProfileFunction import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.logging.LTag import info.nightscout.androidaps.plugins.aps.logger.LoggerCallback +import info.nightscout.androidaps.plugins.aps.loop.APSResult import info.nightscout.androidaps.plugins.aps.loop.ScriptReader import info.nightscout.androidaps.plugins.aps.openAPSSMB.SMBDefaults import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker @@ -30,7 +32,7 @@ import java.nio.charset.StandardCharsets import javax.inject.Inject import kotlin.math.min -class DetermineBasalAdapterAMAJS internal constructor(scriptReader: ScriptReader, injector: HasAndroidInjector) { +class DetermineBasalAdapterAMAJS internal constructor(scriptReader: ScriptReader, injector: HasAndroidInjector) : DetermineBasalAdapterInterface { private val injector: HasAndroidInjector @@ -48,21 +50,15 @@ class DetermineBasalAdapterAMAJS internal constructor(scriptReader: ScriptReader private var currentTemp = JSONObject() private var autosensData = JSONObject() - var currentTempParam: String? = null - private set - var iobDataParam: String? = null - private set - var glucoseStatusParam: String? = null - private set - var profileParam: String? = null - private set - var mealDataParam: String? = null - private set - var scriptDebug = "" - private set + override var currentTempParam: String? = null + override var iobDataParam: String? = null + override var glucoseStatusParam: String? = null + override var profileParam: String? = null + override var mealDataParam: String? = null + override var scriptDebug = "" @Suppress("SpellCheckingInspection") - operator fun invoke(): DetermineBasalResultAMA? { + override operator fun invoke(): APSResult? { aapsLogger.debug(LTag.APS, ">>> Invoking determine_basal <<<") aapsLogger.debug(LTag.APS, "Glucose status: " + glucoseStatus.toString().also { glucoseStatusParam = it }) aapsLogger.debug(LTag.APS, "IOB data: " + iobData.toString().also { iobDataParam = it }) @@ -143,18 +139,25 @@ class DetermineBasalAdapterAMAJS internal constructor(scriptReader: ScriptReader } @Suppress("SpellCheckingInspection") - @Throws(JSONException::class) fun setData(profile: Profile, - maxIob: Double, - maxBasal: Double, - minBg: Double, - maxBg: Double, - targetBg: Double, - basalRate: Double, - iobArray: Array, - glucoseStatus: GlucoseStatus, - mealData: MealData, - autosensDataRatio: Double, - tempTargetSet: Boolean) { + @Throws(JSONException::class) + override fun setData( + profile: Profile, + maxIob: Double, + maxBasal: Double, + minBg: Double, + maxBg: Double, + targetBg: Double, + basalRate: Double, + iobArray: Array, + glucoseStatus: GlucoseStatus, + mealData: MealData, + autosensDataRatio: Double, + tempTargetSet: Boolean, + microBolusAllowed: Boolean, + uamAllowed: Boolean, + advancedFiltering: Boolean, + isSaveCgmSource: Boolean + ) { this.profile = JSONObject() this.profile.put("max_iob", maxIob) this.profile.put("dia", min(profile.dia, 3.0)) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAFragment.kt index 14deed2bb4..d9d8dd2c0b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAFragment.kt @@ -96,7 +96,7 @@ class OpenAPSAMAFragment : DaggerFragment() { binding.result.text = jsonFormatter.format(lastAPSResult.json) binding.request.text = lastAPSResult.toSpanned() } - openAPSAMAPlugin.lastDetermineBasalAdapterAMAJS?.let { determineBasalAdapterAMAJS -> + openAPSAMAPlugin.lastDetermineBasalAdapter?.let { determineBasalAdapterAMAJS -> binding.glucosestatus.text = jsonFormatter.format(determineBasalAdapterAMAJS.glucoseStatusParam) binding.currenttemp.text = jsonFormatter.format(determineBasalAdapterAMAJS.currentTempParam) try { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt index 5c883413ed..229b8ecd14 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt @@ -60,8 +60,8 @@ class OpenAPSAMAPlugin @Inject constructor( // last values override var lastAPSRun: Long = 0 override var lastAPSResult: DetermineBasalResultAMA? = null - var lastDetermineBasalAdapterAMAJS: DetermineBasalAdapterAMAJS? = null - var lastAutosensResult: AutosensResult = AutosensResult() + override var lastDetermineBasalAdapter: DetermineBasalAdapterInterface? = null + override var lastAutosensResult: AutosensResult = AutosensResult() override fun specialEnableCondition(): Boolean { return try { @@ -158,7 +158,7 @@ class OpenAPSAMAPlugin @Inject constructor( // Fix bug determine basal if (determineBasalResultAMA == null) { aapsLogger.error(LTag.APS, "SMB calculation returned null") - lastDetermineBasalAdapterAMAJS = null + lastDetermineBasalAdapter = null lastAPSResult = null lastAPSRun = 0 } else { @@ -167,8 +167,8 @@ class OpenAPSAMAPlugin @Inject constructor( val now = System.currentTimeMillis() determineBasalResultAMA.json?.put("timestamp", dateUtil.toISOString(now)) determineBasalResultAMA.inputConstraints = inputConstraints - lastDetermineBasalAdapterAMAJS = determineBasalAdapterAMAJS - lastAPSResult = determineBasalResultAMA + lastDetermineBasalAdapter = determineBasalAdapterAMAJS + lastAPSResult = determineBasalResultAMA as DetermineBasalResultAMA lastAPSRun = now } rxBus.send(EventOpenAPSUpdateGui()) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt index c498800cec..33c5bb0aa4 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt @@ -7,14 +7,11 @@ import info.nightscout.androidaps.data.MealData import info.nightscout.androidaps.extensions.convertedToAbsolute import info.nightscout.androidaps.extensions.getPassedDurationToTimeInMinutes import info.nightscout.androidaps.extensions.plannedRemainingMinutes -import info.nightscout.androidaps.interfaces.ActivePlugin -import info.nightscout.androidaps.interfaces.GlucoseUnit -import info.nightscout.androidaps.interfaces.IobCobCalculator -import info.nightscout.androidaps.interfaces.Profile -import info.nightscout.androidaps.interfaces.ProfileFunction +import info.nightscout.androidaps.interfaces.* import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.logging.LTag import info.nightscout.androidaps.plugins.aps.logger.LoggerCallback +import info.nightscout.androidaps.plugins.aps.loop.APSResult import info.nightscout.androidaps.plugins.aps.loop.ScriptReader import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus @@ -31,7 +28,7 @@ import java.lang.reflect.InvocationTargetException import java.nio.charset.StandardCharsets import javax.inject.Inject -class DetermineBasalAdapterSMBJS internal constructor(private val scriptReader: ScriptReader, private val injector: HasAndroidInjector) { +class DetermineBasalAdapterSMBJS internal constructor(private val scriptReader: ScriptReader, private val injector: HasAndroidInjector) : DetermineBasalAdapterInterface { @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var constraintChecker: ConstraintChecker @@ -51,21 +48,16 @@ class DetermineBasalAdapterSMBJS internal constructor(private val scriptReader: private var smbAlwaysAllowed = false private var currentTime: Long = 0 private var saveCgmSource = false - var currentTempParam: String? = null - private set - var iobDataParam: String? = null - private set - var glucoseStatusParam: String? = null - private set - var profileParam: String? = null - private set - var mealDataParam: String? = null - private set - var scriptDebug = "" - private set + + override var currentTempParam: String? = null + override var iobDataParam: String? = null + override var glucoseStatusParam: String? = null + override var profileParam: String? = null + override var mealDataParam: String? = null + override var scriptDebug = "" @Suppress("SpellCheckingInspection") - operator fun invoke(): DetermineBasalResultSMB? { + override operator fun invoke(): APSResult? { aapsLogger.debug(LTag.APS, ">>> Invoking determine_basal <<<") aapsLogger.debug(LTag.APS, "Glucose status: " + mGlucoseStatus.toString().also { glucoseStatusParam = it }) aapsLogger.debug(LTag.APS, "IOB data: " + iobData.toString().also { iobDataParam = it }) @@ -155,22 +147,24 @@ class DetermineBasalAdapterSMBJS internal constructor(private val scriptReader: return determineBasalResultSMB } - @Suppress("SpellCheckingInspection") fun setData(profile: Profile, - maxIob: Double, - maxBasal: Double, - minBg: Double, - maxBg: Double, - targetBg: Double, - basalRate: Double, - iobArray: Array, - glucoseStatus: GlucoseStatus, - mealData: MealData, - autosensDataRatio: Double, - tempTargetSet: Boolean, - microBolusAllowed: Boolean, - uamAllowed: Boolean, - advancedFiltering: Boolean, - isSaveCgmSource: Boolean + @Suppress("SpellCheckingInspection") + override fun setData( + profile: Profile, + maxIob: Double, + maxBasal: Double, + minBg: Double, + maxBg: Double, + targetBg: Double, + basalRate: Double, + iobArray: Array, + glucoseStatus: GlucoseStatus, + mealData: MealData, + autosensDataRatio: Double, + tempTargetSet: Boolean, + microBolusAllowed: Boolean, + uamAllowed: Boolean, + advancedFiltering: Boolean, + isSaveCgmSource: Boolean ) { val pump = activePlugin.activePump val pumpBolusStep = pump.pumpDescription.bolusStep diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalResultSMB.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalResultSMB.kt index 03d9c714cc..ecef4ad3aa 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalResultSMB.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/DetermineBasalResultSMB.kt @@ -10,6 +10,7 @@ class DetermineBasalResultSMB private constructor(injector: HasAndroidInjector) private var eventualBG = 0.0 private var snoozeBG = 0.0 + var variableSens: Double? = null internal constructor(injector: HasAndroidInjector, result: JSONObject) : this(injector) { date = dateUtil.now() @@ -50,6 +51,7 @@ class DetermineBasalResultSMB private constructor(injector: HasAndroidInjector) aapsLogger.error(LTag.APS, "Error parsing 'deliverAt' date: $date", e) } } + if (result.has("variable_sens")) variableSens = result.getDouble("variable_sens"); } catch (e: JSONException) { aapsLogger.error(LTag.APS, "Error parsing determine-basal result JSON", e) } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBFragment.kt index b93170d2ac..785066e695 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBFragment.kt @@ -9,8 +9,7 @@ import android.view.ViewGroup import dagger.android.support.DaggerFragment import info.nightscout.androidaps.R import info.nightscout.androidaps.databinding.OpenapsamaFragmentBinding -import info.nightscout.shared.logging.AAPSLogger -import info.nightscout.shared.logging.LTag +import info.nightscout.androidaps.interfaces.ActivePlugin import info.nightscout.androidaps.plugins.aps.events.EventOpenAPSUpdateGui import info.nightscout.androidaps.plugins.aps.events.EventOpenAPSUpdateResultGui import info.nightscout.androidaps.plugins.bus.RxBus @@ -19,6 +18,8 @@ import info.nightscout.androidaps.utils.FabricPrivacy import info.nightscout.androidaps.utils.JSONFormatter import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.rx.AapsSchedulers +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import org.json.JSONArray @@ -34,7 +35,7 @@ class OpenAPSSMBFragment : DaggerFragment() { @Inject lateinit var rxBus: RxBus @Inject lateinit var rh: ResourceHelper @Inject lateinit var fabricPrivacy: FabricPrivacy - @Inject lateinit var openAPSSMBPlugin: OpenAPSSMBPlugin + @Inject lateinit var activePlugin: ActivePlugin @Inject lateinit var dateUtil: DateUtil @Inject lateinit var jsonFormatter: JSONFormatter @@ -44,8 +45,7 @@ class OpenAPSSMBFragment : DaggerFragment() { // onDestroyView. private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = OpenapsamaFragmentBinding.inflate(inflater, container, false) return binding.root } @@ -54,7 +54,7 @@ class OpenAPSSMBFragment : DaggerFragment() { super.onViewCreated(view, savedInstanceState) binding.run.setOnClickListener { - openAPSSMBPlugin.invoke("OpenAPSSMB button", false) + activePlugin.activeAPS.invoke("OpenAPSSMB button", false) } } @@ -92,11 +92,12 @@ class OpenAPSSMBFragment : DaggerFragment() { @Synchronized fun updateGUI() { if (_binding == null) return + val openAPSSMBPlugin = activePlugin.activeAPS openAPSSMBPlugin.lastAPSResult?.let { lastAPSResult -> binding.result.text = jsonFormatter.format(lastAPSResult.json) binding.request.text = lastAPSResult.toSpanned() } - openAPSSMBPlugin.lastDetermineBasalAdapterSMBJS?.let { determineBasalAdapterSMBJS -> + openAPSSMBPlugin.lastDetermineBasalAdapter?.let { determineBasalAdapterSMBJS -> binding.glucosestatus.text = jsonFormatter.format(determineBasalAdapterSMBJS.glucoseStatusParam) binding.currenttemp.text = jsonFormatter.format(determineBasalAdapterSMBJS.currentTempParam) try { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt index 09e79999f4..873b0294e8 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt @@ -10,8 +10,6 @@ import info.nightscout.androidaps.database.AppRepository import info.nightscout.androidaps.database.ValueWrapper import info.nightscout.androidaps.extensions.target import info.nightscout.androidaps.interfaces.* -import info.nightscout.shared.logging.AAPSLogger -import info.nightscout.shared.logging.LTag import info.nightscout.androidaps.plugins.aps.events.EventOpenAPSUpdateGui import info.nightscout.androidaps.plugins.aps.events.EventOpenAPSUpdateResultGui import info.nightscout.androidaps.plugins.aps.loop.ScriptReader @@ -24,6 +22,8 @@ import info.nightscout.androidaps.utils.HardLimits import info.nightscout.androidaps.utils.Profiler import info.nightscout.androidaps.utils.Round import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag import info.nightscout.shared.sharedPreferences.SP import javax.inject.Inject import javax.inject.Singleton @@ -37,7 +37,7 @@ class OpenAPSSMBPlugin @Inject constructor( private val constraintChecker: ConstraintChecker, rh: ResourceHelper, private val profileFunction: ProfileFunction, - private val context: Context, + val context: Context, private val activePlugin: ActivePlugin, private val iobCobCalculator: IobCobCalculator, private val hardLimits: HardLimits, @@ -46,23 +46,24 @@ class OpenAPSSMBPlugin @Inject constructor( private val dateUtil: DateUtil, private val repository: AppRepository, private val glucoseStatusProvider: GlucoseStatusProvider -) : PluginBase(PluginDescription() - .mainType(PluginType.APS) - .fragmentClass(OpenAPSSMBFragment::class.java.name) - .pluginIcon(R.drawable.ic_generic_icon) - .pluginName(R.string.openapssmb) - .shortName(R.string.smb_shortname) - .preferencesId(R.xml.pref_openapssmb) - .description(R.string.description_smb) - .setDefault(), +) : PluginBase( + PluginDescription() + .mainType(PluginType.APS) + .fragmentClass(OpenAPSSMBFragment::class.java.name) + .pluginIcon(R.drawable.ic_generic_icon) + .pluginName(R.string.openapssmb) + .shortName(R.string.smb_shortname) + .preferencesId(R.xml.pref_openapssmb) + .description(R.string.description_smb) + .setDefault(), aapsLogger, rh, injector ), APS, Constraints { // last values override var lastAPSRun: Long = 0 override var lastAPSResult: DetermineBasalResultSMB? = null - var lastDetermineBasalAdapterSMBJS: DetermineBasalAdapterSMBJS? = null - var lastAutosensResult = AutosensResult() + override var lastDetermineBasalAdapter: DetermineBasalAdapterInterface? = null + override var lastAutosensResult = AutosensResult() override fun specialEnableCondition(): Boolean { return try { @@ -120,15 +121,34 @@ class OpenAPSSMBPlugin @Inject constructor( }.value() var minBg = hardLimits.verifyHardLimits(Round.roundTo(profile.getTargetLowMgdl(), 0.1), R.string.profile_low_target, HardLimits.VERY_HARD_LIMIT_MIN_BG[0], HardLimits.VERY_HARD_LIMIT_MIN_BG[1]) - var maxBg = hardLimits.verifyHardLimits(Round.roundTo(profile.getTargetHighMgdl(), 0.1), R.string.profile_high_target, HardLimits.VERY_HARD_LIMIT_MAX_BG[0], HardLimits.VERY_HARD_LIMIT_MAX_BG[1]) + var maxBg = + hardLimits.verifyHardLimits(Round.roundTo(profile.getTargetHighMgdl(), 0.1), R.string.profile_high_target, HardLimits.VERY_HARD_LIMIT_MAX_BG[0], HardLimits.VERY_HARD_LIMIT_MAX_BG[1]) var targetBg = hardLimits.verifyHardLimits(profile.getTargetMgdl(), R.string.temp_target_value, HardLimits.VERY_HARD_LIMIT_TARGET_BG[0], HardLimits.VERY_HARD_LIMIT_TARGET_BG[1]) var isTempTarget = false val tempTarget = repository.getTemporaryTargetActiveAt(dateUtil.now()).blockingGet() if (tempTarget is ValueWrapper.Existing) { isTempTarget = true - minBg = hardLimits.verifyHardLimits(tempTarget.value.lowTarget, R.string.temp_target_low_target, HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[0].toDouble(), HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[1].toDouble()) - maxBg = hardLimits.verifyHardLimits(tempTarget.value.highTarget, R.string.temp_target_high_target, HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[0].toDouble(), HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[1].toDouble()) - targetBg = hardLimits.verifyHardLimits(tempTarget.value.target(), R.string.temp_target_value, HardLimits.VERY_HARD_LIMIT_TEMP_TARGET_BG[0].toDouble(), HardLimits.VERY_HARD_LIMIT_TEMP_TARGET_BG[1].toDouble()) + minBg = + hardLimits.verifyHardLimits( + tempTarget.value.lowTarget, + R.string.temp_target_low_target, + HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[0].toDouble(), + HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[1].toDouble() + ) + maxBg = + hardLimits.verifyHardLimits( + tempTarget.value.highTarget, + R.string.temp_target_high_target, + HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[0].toDouble(), + HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[1].toDouble() + ) + targetBg = + hardLimits.verifyHardLimits( + tempTarget.value.target(), + R.string.temp_target_value, + HardLimits.VERY_HARD_LIMIT_TEMP_TARGET_BG[0].toDouble(), + HardLimits.VERY_HARD_LIMIT_TEMP_TARGET_BG[1].toDouble() + ) } if (!hardLimits.checkHardLimits(profile.dia, R.string.profile_dia, hardLimits.minDia(), hardLimits.maxDia())) return if (!hardLimits.checkHardLimits(profile.getIcTimeFromMidnight(Profile.secondsFromMidnight()), R.string.profile_carbs_ratio_value, hardLimits.minIC(), hardLimits.maxIC())) return @@ -165,8 +185,9 @@ class OpenAPSSMBPlugin @Inject constructor( profiler.log(LTag.APS, "SMB data gathering", start) start = System.currentTimeMillis() - DetermineBasalAdapterSMBJS(ScriptReader(context), injector).also { determineBasalAdapterSMBJS -> - determineBasalAdapterSMBJS.setData(profile, maxIob, maxBasal, minBg, maxBg, targetBg, + provideDetermineBasalAdapter().also { determineBasalAdapterSMBJS -> + determineBasalAdapterSMBJS.setData( + profile, maxIob, maxBasal, minBg, maxBg, targetBg, activePlugin.activePump.baseBasalRate, iobArray, glucoseStatus, @@ -176,24 +197,26 @@ class OpenAPSSMBPlugin @Inject constructor( smbAllowed.value(), uam.value(), advancedFiltering.value(), - activePlugin.activeBgSource.javaClass.simpleName == "DexcomPlugin") + activePlugin.activeBgSource.javaClass.simpleName == "DexcomPlugin" + ) val now = System.currentTimeMillis() val determineBasalResultSMB = determineBasalAdapterSMBJS.invoke() profiler.log(LTag.APS, "SMB calculation", start) if (determineBasalResultSMB == null) { aapsLogger.error(LTag.APS, "SMB calculation returned null") - lastDetermineBasalAdapterSMBJS = null + lastDetermineBasalAdapter = null lastAPSResult = null lastAPSRun = 0 } else { // TODO still needed with oref1? // Fix bug determine basal - if (determineBasalResultSMB.rate == 0.0 && determineBasalResultSMB.duration == 0 && iobCobCalculator.getTempBasalIncludingConvertedExtended(dateUtil.now()) == null) determineBasalResultSMB.tempBasalRequested = false + if (determineBasalResultSMB.rate == 0.0 && determineBasalResultSMB.duration == 0 && iobCobCalculator.getTempBasalIncludingConvertedExtended(dateUtil.now()) == null) determineBasalResultSMB.tempBasalRequested = + false determineBasalResultSMB.iob = iobArray[0] determineBasalResultSMB.json?.put("timestamp", dateUtil.toISOString(now)) determineBasalResultSMB.inputConstraints = inputConstraints - lastDetermineBasalAdapterSMBJS = determineBasalAdapterSMBJS - lastAPSResult = determineBasalResultSMB + lastDetermineBasalAdapter = determineBasalAdapterSMBJS + lastAPSResult = determineBasalResultSMB as DetermineBasalResultSMB lastAPSRun = now } } @@ -204,4 +227,6 @@ class OpenAPSSMBPlugin @Inject constructor( value.set(aapsLogger, false) return value } + + fun provideDetermineBasalAdapter(): DetermineBasalAdapterInterface = DetermineBasalAdapterSMBJS(ScriptReader(context), injector) } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt new file mode 100644 index 0000000000..950a3e9f26 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt @@ -0,0 +1,296 @@ +package info.nightscout.androidaps.plugins.aps.openAPSSMBDynamicISF + +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.R +import info.nightscout.androidaps.data.IobTotal +import info.nightscout.androidaps.data.MealData +import info.nightscout.androidaps.database.AppRepository +import info.nightscout.androidaps.extensions.convertedToAbsolute +import info.nightscout.androidaps.extensions.getPassedDurationToTimeInMinutes +import info.nightscout.androidaps.extensions.plannedRemainingMinutes +import info.nightscout.androidaps.interfaces.ActivePlugin +import info.nightscout.androidaps.interfaces.GlucoseUnit +import info.nightscout.androidaps.interfaces.IobCobCalculator +import info.nightscout.androidaps.interfaces.Profile +import info.nightscout.androidaps.interfaces.ProfileFunction +import info.nightscout.androidaps.plugins.aps.logger.LoggerCallback +import info.nightscout.androidaps.plugins.aps.loop.ScriptReader +import info.nightscout.androidaps.interfaces.DetermineBasalAdapterInterface +import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalResultSMB +import info.nightscout.androidaps.plugins.aps.openAPSSMB.SMBDefaults +import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.stats.TddCalculator +import info.nightscout.shared.SafeParse +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.logging.LTag +import info.nightscout.shared.sharedPreferences.SP +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.mozilla.javascript.* +import org.mozilla.javascript.Function +import java.io.IOException +import java.lang.reflect.InvocationTargetException +import java.nio.charset.StandardCharsets +import javax.inject.Inject + +class DetermineBasalAdapterSMBDynamicISFJS internal constructor(private val scriptReader: ScriptReader, private val injector: HasAndroidInjector) : DetermineBasalAdapterInterface { + + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var constraintChecker: ConstraintChecker + @Inject lateinit var sp: SP + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var iobCobCalculator: IobCobCalculator + @Inject lateinit var activePlugin: ActivePlugin + @Inject lateinit var repository: AppRepository + @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var tddCalculator: TddCalculator + + private var profile = JSONObject() + private var mGlucoseStatus = JSONObject() + private var iobData: JSONArray? = null + private var mealData = JSONObject() + private var currentTemp = JSONObject() + private var autosensData = JSONObject() + private var microBolusAllowed = false + private var smbAlwaysAllowed = false + private var currentTime: Long = 0 + private var saveCgmSource = false + + override var currentTempParam: String? = null + override var iobDataParam: String? = null + override var glucoseStatusParam: String? = null + override var profileParam: String? = null + override var mealDataParam: String? = null + override var scriptDebug = "" + + @Suppress("SpellCheckingInspection") + override operator fun invoke(): DetermineBasalResultSMB? { + aapsLogger.debug(LTag.APS, ">>> Invoking determine_basal <<<") + aapsLogger.debug(LTag.APS, "Glucose status: " + mGlucoseStatus.toString().also { glucoseStatusParam = it }) + aapsLogger.debug(LTag.APS, "IOB data: " + iobData.toString().also { iobDataParam = it }) + aapsLogger.debug(LTag.APS, "Current temp: " + currentTemp.toString().also { currentTempParam = it }) + aapsLogger.debug(LTag.APS, "Profile: " + profile.toString().also { profileParam = it }) + aapsLogger.debug(LTag.APS, "Meal data: " + mealData.toString().also { mealDataParam = it }) + aapsLogger.debug(LTag.APS, "Autosens data: $autosensData") + aapsLogger.debug(LTag.APS, "Reservoir data: " + "undefined") + aapsLogger.debug(LTag.APS, "MicroBolusAllowed: $microBolusAllowed") + aapsLogger.debug(LTag.APS, "SMBAlwaysAllowed: $smbAlwaysAllowed") + aapsLogger.debug(LTag.APS, "CurrentTime: $currentTime") + aapsLogger.debug(LTag.APS, "isSaveCgmSource: $saveCgmSource") + var determineBasalResultSMB: DetermineBasalResultSMB? = null + val rhino = Context.enter() + val scope: Scriptable = rhino.initStandardObjects() + // Turn off optimization to make Rhino Android compatible + rhino.optimizationLevel = -1 + try { + + //register logger callback for console.log and console.error + ScriptableObject.defineClass(scope, LoggerCallback::class.java) + val myLogger = rhino.newObject(scope, "LoggerCallback", null) + scope.put("console2", scope, myLogger) + rhino.evaluateString(scope, readFile("OpenAPSAMA/loggerhelper.js"), "JavaScript", 0, null) + + //set module parent + rhino.evaluateString(scope, "var module = {\"parent\":Boolean(1)};", "JavaScript", 0, null) + rhino.evaluateString(scope, "var round_basal = function round_basal(basal, profile) { return basal; };", "JavaScript", 0, null) + rhino.evaluateString(scope, "require = function() {return round_basal;};", "JavaScript", 0, null) + + //generate functions "determine_basal" and "setTempBasal" + rhino.evaluateString(scope, readFile("OpenAPSSMBDynamicISF/determine-basal.js"), "JavaScript", 0, null) + rhino.evaluateString(scope, readFile("OpenAPSSMB/basal-set-temp.js"), "setTempBasal.js", 0, null) + val determineBasalObj = scope["determine_basal", scope] + val setTempBasalFunctionsObj = scope["tempBasalFunctions", scope] + + //call determine-basal + if (determineBasalObj is Function && setTempBasalFunctionsObj is NativeObject) { + + //prepare parameters + val params = arrayOf( + makeParam(mGlucoseStatus, rhino, scope), + makeParam(currentTemp, rhino, scope), + makeParamArray(iobData, rhino, scope), + makeParam(profile, rhino, scope), + makeParam(autosensData, rhino, scope), + makeParam(mealData, rhino, scope), + setTempBasalFunctionsObj, + java.lang.Boolean.valueOf(microBolusAllowed), + makeParam(null, rhino, scope), // reservoir data as undefined + java.lang.Long.valueOf(currentTime), + java.lang.Boolean.valueOf(saveCgmSource) + ) + val jsResult = determineBasalObj.call(rhino, scope, scope, params) as NativeObject + scriptDebug = LoggerCallback.scriptDebug + + // Parse the jsResult object to a JSON-String + val result = NativeJSON.stringify(rhino, scope, jsResult, null, null).toString() + aapsLogger.debug(LTag.APS, "Result: $result") + try { + val resultJson = JSONObject(result) + determineBasalResultSMB = DetermineBasalResultSMB(injector, resultJson) + } catch (e: JSONException) { + aapsLogger.error(LTag.APS, "Unhandled exception", e) + } + } else { + aapsLogger.error(LTag.APS, "Problem loading JS Functions") + } + } catch (e: IOException) { + aapsLogger.error(LTag.APS, "IOException") + } catch (e: RhinoException) { + aapsLogger.error(LTag.APS, "RhinoException: (" + e.lineNumber() + "," + e.columnNumber() + ") " + e.toString()) + } catch (e: IllegalAccessException) { + aapsLogger.error(LTag.APS, e.toString()) + } catch (e: InstantiationException) { + aapsLogger.error(LTag.APS, e.toString()) + } catch (e: InvocationTargetException) { + aapsLogger.error(LTag.APS, e.toString()) + } finally { + Context.exit() + } + glucoseStatusParam = mGlucoseStatus.toString() + iobDataParam = iobData.toString() + currentTempParam = currentTemp.toString() + profileParam = profile.toString() + mealDataParam = mealData.toString() + return determineBasalResultSMB + } + + @Suppress("SpellCheckingInspection") + override fun setData( + profile: Profile, + maxIob: Double, + maxBasal: Double, + minBg: Double, + maxBg: Double, + targetBg: Double, + basalRate: Double, + iobArray: Array, + glucoseStatus: GlucoseStatus, + mealData: MealData, + autosensDataRatio: Double, + tempTargetSet: Boolean, + microBolusAllowed: Boolean, + uamAllowed: Boolean, + advancedFiltering: Boolean, + isSaveCgmSource: Boolean + ) { + val pump = activePlugin.activePump + val pumpBolusStep = pump.pumpDescription.bolusStep + this.profile.put("max_iob", maxIob) + //mProfile.put("dia", profile.getDia()); + this.profile.put("type", "current") + this.profile.put("max_daily_basal", profile.getMaxDailyBasal()) + this.profile.put("max_basal", maxBasal) + this.profile.put("min_bg", minBg) + this.profile.put("max_bg", maxBg) + this.profile.put("target_bg", targetBg) + this.profile.put("carb_ratio", profile.getIc()) + this.profile.put("sens", profile.getIsfMgdl()) + this.profile.put("max_daily_safety_multiplier", sp.getInt(R.string.key_openapsama_max_daily_safety_multiplier, 3)) + this.profile.put("current_basal_safety_multiplier", sp.getDouble(R.string.key_openapsama_current_basal_safety_multiplier, 4.0)) + + //mProfile.put("high_temptarget_raises_sensitivity", SP.getBoolean(R.string.key_high_temptarget_raises_sensitivity, SMBDefaults.high_temptarget_raises_sensitivity)); + this.profile.put("high_temptarget_raises_sensitivity", false) + //mProfile.put("low_temptarget_lowers_sensitivity", SP.getBoolean(R.string.key_low_temptarget_lowers_sensitivity, SMBDefaults.low_temptarget_lowers_sensitivity)); + this.profile.put("low_temptarget_lowers_sensitivity", false) + this.profile.put("sensitivity_raises_target", sp.getBoolean(R.string.key_sensitivity_raises_target, SMBDefaults.sensitivity_raises_target)) + this.profile.put("resistance_lowers_target", sp.getBoolean(R.string.key_resistance_lowers_target, SMBDefaults.resistance_lowers_target)) + this.profile.put("adv_target_adjustments", SMBDefaults.adv_target_adjustments) + this.profile.put("exercise_mode", SMBDefaults.exercise_mode) + this.profile.put("half_basal_exercise_target", SMBDefaults.half_basal_exercise_target) + this.profile.put("maxCOB", SMBDefaults.maxCOB) + this.profile.put("skip_neutral_temps", pump.setNeutralTempAtFullHour()) + // min_5m_carbimpact is not used within SMB determinebasal + //if (mealData.usedMinCarbsImpact > 0) { + // mProfile.put("min_5m_carbimpact", mealData.usedMinCarbsImpact); + //} else { + // mProfile.put("min_5m_carbimpact", SP.getDouble(R.string.key_openapsama_min_5m_carbimpact, SMBDefaults.min_5m_carbimpact)); + //} + this.profile.put("remainingCarbsCap", SMBDefaults.remainingCarbsCap) + this.profile.put("enableUAM", uamAllowed) + this.profile.put("A52_risk_enable", SMBDefaults.A52_risk_enable) + val smbEnabled = sp.getBoolean(R.string.key_use_smb, false) + this.profile.put("SMBInterval", sp.getInt(R.string.key_smbinterval, SMBDefaults.SMBInterval)) + this.profile.put("enableSMB_with_COB", smbEnabled && sp.getBoolean(R.string.key_enableSMB_with_COB, false)) + this.profile.put("enableSMB_with_temptarget", smbEnabled && sp.getBoolean(R.string.key_enableSMB_with_temptarget, false)) + this.profile.put("allowSMB_with_high_temptarget", smbEnabled && sp.getBoolean(R.string.key_allowSMB_with_high_temptarget, false)) + this.profile.put("enableSMB_always", smbEnabled && sp.getBoolean(R.string.key_enableSMB_always, false) && advancedFiltering) + this.profile.put("enableSMB_after_carbs", smbEnabled && sp.getBoolean(R.string.key_enableSMB_after_carbs, false) && advancedFiltering) + this.profile.put("maxSMBBasalMinutes", sp.getInt(R.string.key_smbmaxminutes, SMBDefaults.maxSMBBasalMinutes)) + this.profile.put("maxUAMSMBBasalMinutes", sp.getInt(R.string.key_uamsmbmaxminutes, SMBDefaults.maxUAMSMBBasalMinutes)) + //set the min SMB amount to be the amount set by the pump. + this.profile.put("bolus_increment", pumpBolusStep) + this.profile.put("carbsReqThreshold", sp.getInt(R.string.key_carbsReqThreshold, SMBDefaults.carbsReqThreshold)) + this.profile.put("current_basal", basalRate) + this.profile.put("temptargetSet", tempTargetSet) + this.profile.put("autosens_max", SafeParse.stringToDouble(sp.getString(R.string.key_openapsama_autosens_max, "1.2"))) + if (profileFunction.getUnits() == GlucoseUnit.MMOL) { + this.profile.put("out_units", "mmol/L") + } + val now = System.currentTimeMillis() + val tb = iobCobCalculator.getTempBasalIncludingConvertedExtended(now) + currentTemp.put("temp", "absolute") + currentTemp.put("duration", tb?.plannedRemainingMinutes ?: 0) + currentTemp.put("rate", tb?.convertedToAbsolute(now, profile) ?: 0.0) + // as we have non default temps longer than 30 mintues + if (tb != null) currentTemp.put("minutesrunning", tb.getPassedDurationToTimeInMinutes(now)) + + iobData = iobCobCalculator.convertToJSONArray(iobArray) + mGlucoseStatus.put("glucose", glucoseStatus.glucose) + mGlucoseStatus.put("noise", glucoseStatus.noise) + if (sp.getBoolean(R.string.key_always_use_shortavg, false)) { + mGlucoseStatus.put("delta", glucoseStatus.shortAvgDelta) + } else { + mGlucoseStatus.put("delta", glucoseStatus.delta) + } + + mGlucoseStatus.put("short_avgdelta", glucoseStatus.shortAvgDelta) + mGlucoseStatus.put("long_avgdelta", glucoseStatus.longAvgDelta) + mGlucoseStatus.put("date", glucoseStatus.date) + this.mealData.put("carbs", mealData.carbs) + this.mealData.put("mealCOB", mealData.mealCOB) + this.mealData.put("slopeFromMaxDeviation", mealData.slopeFromMaxDeviation) + this.mealData.put("slopeFromMinDeviation", mealData.slopeFromMinDeviation) + this.mealData.put("lastBolusTime", mealData.lastBolusTime) + this.mealData.put("lastCarbTime", mealData.lastCarbTime) + + this.mealData.put("TDDAIMI7", tddCalculator.averageTDD(tddCalculator.calculate(7)).totalAmount) + this.mealData.put("TDDPUMP", tddCalculator.calculateDaily().totalAmount) + + if (constraintChecker.isAutosensModeEnabled().value()) { + autosensData.put("ratio", autosensDataRatio) + } else { + autosensData.put("ratio", 1.0) + } + this.microBolusAllowed = microBolusAllowed + smbAlwaysAllowed = advancedFiltering + currentTime = now + saveCgmSource = isSaveCgmSource + } + + private fun makeParam(jsonObject: JSONObject?, rhino: Context, scope: Scriptable): Any { + return if (jsonObject == null) Undefined.instance + else NativeJSON.parse(rhino, scope, jsonObject.toString()) { _: Context?, _: Scriptable?, _: Scriptable?, objects: Array -> objects[1] } + } + + private fun makeParamArray(jsonArray: JSONArray?, rhino: Context, scope: Scriptable): Any { + return NativeJSON.parse(rhino, scope, jsonArray.toString()) { _: Context?, _: Scriptable?, _: Scriptable?, objects: Array -> objects[1] } + } + + @Throws(IOException::class) private fun readFile(filename: String): String { + val bytes = scriptReader.readFile(filename) + var string = String(bytes, StandardCharsets.UTF_8) + if (string.startsWith("#!/usr/bin/env node")) { + string = string.substring(20) + } + return string + } + + init { + injector.androidInjector().inject(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMBDynamicISF/OpenAPSSMBDynamicISFPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMBDynamicISF/OpenAPSSMBDynamicISFPlugin.kt new file mode 100644 index 0000000000..8c80b90738 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/aps/openAPSSMBDynamicISF/OpenAPSSMBDynamicISFPlugin.kt @@ -0,0 +1,74 @@ +package info.nightscout.androidaps.plugins.aps.openAPSSMBDynamicISF + +import android.content.Context +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.R +import info.nightscout.androidaps.annotations.OpenForTesting +import info.nightscout.androidaps.database.AppRepository +import info.nightscout.androidaps.interfaces.ActivePlugin +import info.nightscout.androidaps.interfaces.IobCobCalculator +import info.nightscout.androidaps.interfaces.ProfileFunction +import info.nightscout.androidaps.plugins.aps.loop.ScriptReader +import info.nightscout.androidaps.interfaces.DetermineBasalAdapterInterface +import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatusProvider +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.HardLimits +import info.nightscout.androidaps.utils.Profiler +import info.nightscout.androidaps.utils.buildHelper.BuildHelper +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.shared.logging.AAPSLogger +import info.nightscout.shared.sharedPreferences.SP +import javax.inject.Inject +import javax.inject.Singleton + +@OpenForTesting +@Singleton +class OpenAPSSMBDynamicISFPlugin @Inject constructor( + injector: HasAndroidInjector, + aapsLogger: AAPSLogger, + rxBus: RxBus, + constraintChecker: ConstraintChecker, + rh: ResourceHelper, + profileFunction: ProfileFunction, + context: Context, + activePlugin: ActivePlugin, + iobCobCalculator: IobCobCalculator, + hardLimits: HardLimits, + profiler: Profiler, + sp: SP, + dateUtil: DateUtil, + repository: AppRepository, + glucoseStatusProvider: GlucoseStatusProvider, + private val buildHelper: BuildHelper +) : OpenAPSSMBPlugin( + injector, + aapsLogger, + rxBus, + constraintChecker, + rh, + profileFunction, + context, + activePlugin, + iobCobCalculator, + hardLimits, + profiler, + sp, + dateUtil, + repository, + glucoseStatusProvider +) { + + init { + pluginDescription + .pluginName(R.string.openaps_smb_dynamic_isf) + .description(R.string.description_smb_dynamic_isf) + .setDefault(false) + } + + override fun specialEnableCondition(): Boolean = buildHelper.isEngineeringMode() && buildHelper.isDev() + + override fun provideDetermineBasalAdapter(): DetermineBasalAdapterInterface = DetermineBasalAdapterSMBDynamicISFJS(ScriptReader(context), injector) +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt index b45feaea20..67f6c4e13b 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.kt @@ -48,6 +48,7 @@ import info.nightscout.androidaps.interfaces.* import info.nightscout.shared.logging.AAPSLogger import info.nightscout.androidaps.logging.UserEntryLogger import info.nightscout.androidaps.plugins.aps.loop.events.EventNewOpenLoopNotification +import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalResultSMB import info.nightscout.androidaps.plugins.bus.RxBus import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker import info.nightscout.androidaps.plugins.constraints.bgQualityCheck.BgQualityCheckPlugin @@ -603,7 +604,7 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { binding.infoLayout.apsMode.stateDescription = rh.gs(stringRes) } else { - binding.infoLayout.apsMode.contentDescription = rh.gs(R.string.apsmode_title) + " " + rh.gs(stringRes) + binding.infoLayout.apsMode.contentDescription = rh.gs(R.string.apsmode_title) + " " + rh.gs(stringRes) } } @@ -671,6 +672,21 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList binding.infoLayout.apsModeText.visibility = View.GONE } } + // Show variable sensitivity + val request = loop.lastRun?.request + if (request is DetermineBasalResultSMB) { + val isfMgdl = profileFunction.getProfile()?.getIsfMgdl() + val variableSens = request.variableSens + if (variableSens != isfMgdl && variableSens != null && isfMgdl != null) { + binding.infoLayout.variableSensitivity.text = + String.format( + Locale.getDefault(), "%1$.1f→%2$.1f", + Profile.toUnits(isfMgdl, isfMgdl * Constants.MGDL_TO_MMOLL, profileFunction.getUnits()), + Profile.toUnits(variableSens, variableSens * Constants.MGDL_TO_MMOLL, profileFunction.getUnits()) + ) + binding.infoLayout.variableSensitivity.visibility = View.VISIBLE + } else binding.infoLayout.variableSensitivity.visibility = View.GONE + } else binding.infoLayout.variableSensitivity.visibility = View.GONE } else { //nsclient binding.infoLayout.apsMode.visibility = View.GONE @@ -774,7 +790,7 @@ class OverviewFragment : DaggerFragment(), View.OnClickListener, OnLongClickList val outDate = (if (!overviewData.isActualBg) rh.gs(R.string.a11y_bg_outdated) else "") binding.infoLayout.bg.contentDescription = - rh.gs(R.string.a11y_blood_glucose) + " " + binding.infoLayout.bg.text.toString() + " " + overviewData.lastBgDescription + " " + outDate + rh.gs(R.string.a11y_blood_glucose) + " " + binding.infoLayout.bg.text.toString() + " " + overviewData.lastBgDescription + " " + outDate binding.infoLayout.timeAgo.text = dateUtil.minAgo(rh, overviewData.lastBg?.timestamp) binding.infoLayout.timeAgo.contentDescription = dateUtil.minAgoLong(rh, overviewData.lastBg?.timestamp) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt index eb56d8067e..065a2f1fc3 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt @@ -1,12 +1,18 @@ package info.nightscout.androidaps.plugins.general.overview.activities +import android.annotation.SuppressLint import android.os.Bundle +import android.util.Log import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.* import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import info.nightscout.androidaps.R @@ -20,6 +26,8 @@ import info.nightscout.androidaps.utils.FabricPrivacy import io.reactivex.rxkotlin.plusAssign import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.androidaps.utils.wizard.QuickWizard +import info.nightscout.androidaps.utils.wizard.QuickWizardEntry +import info.nightscout.shared.sharedPreferences.SP import io.reactivex.disposables.CompositeDisposable import javax.inject.Inject @@ -30,20 +38,95 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var quickWizard: QuickWizard @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var sp: SP private var disposable: CompositeDisposable = CompositeDisposable() private lateinit var binding: OverviewQuickwizardlistActivityBinding + private val itemTouchHelper by lazy { + val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN or START or END, 0) { + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as RecyclerViewAdapter + val from = viewHolder.layoutPosition + val to = target.layoutPosition + adapter.moveItem(from, to) + adapter.notifyItemMoved(from, to) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + + if (actionState == ACTION_STATE_DRAG) { + viewHolder?.itemView?.alpha = 0.5f + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + viewHolder.itemView.alpha = 1.0f + + val adapter = recyclerView.adapter as RecyclerViewAdapter + adapter.onDrop() + } + } + + ItemTouchHelper(simpleItemTouchCallback) + } + + fun startDragging(viewHolder: RecyclerView.ViewHolder) { + itemTouchHelper.startDrag(viewHolder) + } + private inner class RecyclerViewAdapter(var fragmentManager: FragmentManager) : RecyclerView.Adapter() { + @SuppressLint("ClickableViewAccessibility") override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuickWizardEntryViewHolder { - return QuickWizardEntryViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.overview_quickwizardlist_item, parent, false), fragmentManager) + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.overview_quickwizardlist_item, parent, false) + val viewHolder = QuickWizardEntryViewHolder(itemView, fragmentManager) + + viewHolder.handleView.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + startDragging(viewHolder) + } + return@setOnTouchListener true + } + + return viewHolder } override fun onBindViewHolder(holder: QuickWizardEntryViewHolder, position: Int) { holder.from.text = dateUtil.timeString(quickWizard[position].validFromDate()) holder.to.text = dateUtil.timeString(quickWizard[position].validToDate()) + val wearControl = sp.getBoolean(R.string.key_wear_control, false) + + if (wearControl) { + holder.handleView.visibility = View.VISIBLE + } else { + holder.handleView.visibility = View.GONE + } + if (quickWizard[position].device() == QuickWizardEntry.DEVICE_ALL) { + holder.device.visibility = View.GONE + } else { + holder.device.visibility = View.VISIBLE + holder.device.setImageResource( + when (quickWizard[position].device()) { + QuickWizardEntry.DEVICE_WATCH -> R.drawable.ic_watch + else -> R.drawable.ic_smartphone + } + ) + } holder.buttonText.text = quickWizard[position].buttonText() holder.carbs.text = rh.gs(R.string.format_carbs, quickWizard[position].carbs()) } @@ -55,6 +138,8 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { val buttonText: TextView = itemView.findViewById(R.id.overview_quickwizard_item_buttonText) val carbs: TextView = itemView.findViewById(R.id.overview_quickwizard_item_carbs) val from: TextView = itemView.findViewById(R.id.overview_quickwizard_item_from) + val handleView: ImageView = itemView.findViewById(R.id.handleView) + val device: ImageView = itemView.findViewById(R.id.overview_quickwizard_item_device) val to: TextView = itemView.findViewById(R.id.overview_quickwizard_item_to) private val editButton: Button = itemView.findViewById(R.id.overview_quickwizard_item_edit_button) private val removeButton: Button = itemView.findViewById(R.id.overview_quickwizard_item_remove_button) @@ -74,6 +159,16 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { } } } + + fun moveItem(from: Int, to: Int) { + Log.i("QuickWizard", "moveItem") + quickWizard.move(from, to) + } + + fun onDrop() { + Log.i("QuickWizard", "onDrop") + rxBus.send(EventQuickWizardChange()) + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -84,6 +179,7 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { binding.recyclerview.setHasFixedSize(true) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = RecyclerViewAdapter(supportFragmentManager) + itemTouchHelper.attachToRecyclerView(binding.recyclerview) binding.addButton.setOnClickListener { val manager = supportFragmentManager @@ -98,13 +194,13 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { .toObservable(EventQuickWizardChange::class.java) .observeOn(aapsSchedulers.main) .subscribe({ - val adapter = RecyclerViewAdapter(supportFragmentManager) - binding.recyclerview.swapAdapter(adapter, false) - }, fabricPrivacy::logException) + val adapter = RecyclerViewAdapter(supportFragmentManager) + binding.recyclerview.swapAdapter(adapter, false) + }, fabricPrivacy::logException) } override fun onPause() { disposable.clear() super.onPause() } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt index bfe47b2bdf..e7abb4690d 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.view.Window import android.view.WindowManager import dagger.android.support.DaggerDialogFragment +import info.nightscout.androidaps.R import info.nightscout.androidaps.databinding.OverviewEditquickwizardDialogBinding import info.nightscout.shared.logging.AAPSLogger import info.nightscout.androidaps.plugins.bus.RxBus @@ -21,6 +22,7 @@ import info.nightscout.androidaps.utils.extensions.setEnableForChildren import info.nightscout.androidaps.utils.extensions.setSelection import info.nightscout.androidaps.utils.wizard.QuickWizard import info.nightscout.androidaps.utils.wizard.QuickWizardEntry +import info.nightscout.shared.sharedPreferences.SP import org.json.JSONException import javax.inject.Inject @@ -30,9 +32,9 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var quickWizard: QuickWizard @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var sp: SP var position = -1 - var fromSeconds: Int = 0 var toSeconds: Int = 0 @@ -42,8 +44,10 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { // onDestroyView. private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) isCancelable = true @@ -57,6 +61,14 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { position = bundle.getInt("position", -1) } val entry = if (position == -1) quickWizard.newEmptyItem() else quickWizard[position] + if (sp.getBoolean(R.string.key_wear_control, false)) { + binding.deviceLabel.visibility = View.VISIBLE + binding.device.visibility = View.VISIBLE + } else { + binding.deviceLabel.visibility = View.GONE + binding.device.visibility = View.GONE + } + binding.okcancel.ok.setOnClickListener { try { entry.storage.put("buttonText", binding.buttonEdit.text.toString()) @@ -66,10 +78,14 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { entry.storage.put("useBG", binding.useBg.selectedItemPosition) entry.storage.put("useCOB", binding.useCob.selectedItemPosition) entry.storage.put("useBolusIOB", binding.useBolusIob.selectedItemPosition) + entry.storage.put("device", binding.device.selectedItemPosition) entry.storage.put("useBasalIOB", binding.useBasalIob.selectedItemPosition) entry.storage.put("useTrend", binding.useTrend.selectedItemPosition) entry.storage.put("useSuperBolus", binding.useSuperBolus.selectedItemPosition) entry.storage.put("useTempTarget", binding.useTempTarget.selectedItemPosition) + entry.storage.put("usePercentage", binding.usePercentage.selectedItemPosition) + val percentage = SafeParse.stringToInt(binding.percentage.text.toString()) + entry.storage.put("percentage", percentage) } catch (e: JSONException) { aapsLogger.error("Unhandled exception", e) } @@ -88,7 +104,8 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { binding.from.setOnClickListener { context?.let { - TimePickerDialog(it, fromTimeSetListener, + TimePickerDialog( + it, fromTimeSetListener, T.secs(fromSeconds.toLong()).hours().toInt(), T.secs((fromSeconds % 3600).toLong()).mins().toInt(), DateFormat.is24HourFormat(context) @@ -105,13 +122,29 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { binding.to.setOnClickListener { context?.let { - TimePickerDialog(it, toTimeSetListener, + TimePickerDialog( + it, toTimeSetListener, T.secs(toSeconds.toLong()).hours().toInt(), T.secs((toSeconds % 3600).toLong()).mins().toInt(), DateFormat.is24HourFormat(context) ).show() } } + + fun usePercentage(custom: Boolean) { + if (custom) { + binding.percentageLabel.visibility = View.VISIBLE + binding.percentage.visibility = View.VISIBLE + } else { + binding.percentageLabel.visibility = View.GONE + binding.percentage.visibility = View.GONE + } + } + + binding.usePercentage.setOnCheckedChangeListener { _, checkedId -> + usePercentage(checkedId == R.id.use_percentage_custom) + } + toSeconds = entry.validTo() binding.to.text = dateUtil.timeString(dateUtil.secondsOfTheDayToMilliseconds(toSeconds)) @@ -122,10 +155,13 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { binding.useCob.setSelection(entry.useCOB()) binding.useBolusIob.setSelection(entry.useBolusIOB()) binding.useBasalIob.setSelection(entry.useBasalIOB()) + binding.device.setSelection(entry.device()) binding.useTrend.setSelection(entry.useTrend()) binding.useSuperBolus.setSelection(entry.useSuperBolus()) binding.useTempTarget.setSelection(entry.useTempTarget()) - + binding.usePercentage.setSelection(entry.usePercentage()) + usePercentage(entry.usePercentage() == QuickWizardEntry.CUSTOM) + binding.percentage.setText(entry.percentage().toString()) binding.useCobYes.setOnClickListener(this) binding.useCobNo.setOnClickListener(this) processCob() diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt index f463a10339..124f00e4ac 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt @@ -2,6 +2,7 @@ package info.nightscout.androidaps.plugins.general.wear import android.app.NotificationManager import android.content.Context +import android.util.Log import dagger.android.HasAndroidInjector import info.nightscout.androidaps.Constants import info.nightscout.androidaps.R @@ -39,11 +40,11 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.shared.sharedPreferences.SP import info.nightscout.androidaps.utils.wizard.BolusWizard +import info.nightscout.androidaps.utils.wizard.QuickWizard import info.nightscout.shared.SafeParse import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import java.text.DateFormat -import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit @@ -71,6 +72,7 @@ class ActionStringHandler @Inject constructor( private val activePlugin: ActivePlugin, private val iobCobCalculator: IobCobCalculator, private val localInsightPlugin: LocalInsightPlugin, + private val quickWizard: QuickWizard, private val danaRPlugin: DanaRPlugin, private val danaRKoreanPlugin: DanaRKoreanPlugin, private val danaRv2Plugin: DanaRv2Plugin, @@ -79,7 +81,8 @@ class ActionStringHandler @Inject constructor( private val dateUtil: DateUtil, private val config: Config, private val repository: AppRepository, - private val uel: UserEntryLogger + private val uel: UserEntryLogger, + private val defaultValueHelper: DefaultValueHelper ) { private val timeout = 65 * 1000 @@ -107,9 +110,11 @@ class ActionStringHandler @Inject constructor( @Synchronized private fun handleInitiate(actionString: String) { + //TODO: i18n + Log.i("ActionStringHandler", "handleInitiate actionString=" + actionString) if (!sp.getBoolean(R.string.key_wear_control, false)) return lastBolusWizard = null - var rTitle = "CONFIRM" //TODO: i18n + var rTitle = rh.gs(R.string.confirm).uppercase() var rMessage = "" var rAction = "" // do the parsing and check constraints @@ -136,6 +141,11 @@ class ActionStringHandler @Inject constructor( val carbs = SafeParse.stringToInt(act[2]) val insulinAfterConstraints = constraintChecker.applyBolusConstraints(Constraint(insulin)).value() val carbsAfterConstraints = constraintChecker.applyCarbsConstraints(Constraint(carbs)).value() + val pump = activePlugin.activePump + if (insulinAfterConstraints > 0 && (!pump.isInitialized() || pump.isSuspended() || loop.isDisconnected)) { + sendError(rh.gs(R.string.wizard_pump_not_available)) + return + } rMessage += rh.gs(R.string.bolus) + ": " + insulinAfterConstraints + "U\n" rMessage += rh.gs(R.string.carbs) + ": " + carbsAfterConstraints + "g" if (insulinAfterConstraints - insulin != 0.0 || carbsAfterConstraints - carbs != 0) { @@ -143,32 +153,72 @@ class ActionStringHandler @Inject constructor( } rAction += "bolus $insulinAfterConstraints $carbsAfterConstraints" } else if ("temptarget" == act[0]) { ///////////////////////////////////////////////////////// TEMPTARGET - val isMGDL = java.lang.Boolean.parseBoolean(act[1]) - if (profileFunction.getUnits() == GlucoseUnit.MGDL != isMGDL) { - sendError("Different units used on watch and phone!") - return - } - val duration = SafeParse.stringToInt(act[2]) - if (duration == 0) { - rMessage += "Zero-Temp-Target - cancelling running Temp-Targets?" + aapsLogger.info(LTag.WEAR, "temptarget received: $act") + if ("cancel" == act[1]) { + rMessage += rh.gs(R.string.wear_action_tempt_cancel_message) rAction = "temptarget true 0 0 0" + } else if ("preset" == act[1]) { + val presetIsMGDL = profileFunction.getUnits() == GlucoseUnit.MGDL + val preset = act[2] + when (preset) { + "activity" -> { + val activityTTDuration = defaultValueHelper.determineActivityTTDuration() + val activityTT = defaultValueHelper.determineActivityTT() + val reason = rh.gs(R.string.activity) + rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, activityTT, activityTTDuration) + rAction = "temptarget $presetIsMGDL $activityTTDuration $activityTT $activityTT" + } + + "hypo" -> { + val hypoTTDuration = defaultValueHelper.determineHypoTTDuration() + val hypoTT = defaultValueHelper.determineHypoTT() + val reason = rh.gs(R.string.hypo) + rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, hypoTT, hypoTTDuration) + rAction = "temptarget $presetIsMGDL $hypoTTDuration $hypoTT $hypoTT" + } + + "eating" -> { + val eatingSoonTTDuration = defaultValueHelper.determineEatingSoonTTDuration() + val eatingSoonTT = defaultValueHelper.determineEatingSoonTT() + val reason = rh.gs(R.string.eatingsoon) + rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, eatingSoonTT, eatingSoonTTDuration) + rAction = "temptarget $presetIsMGDL $eatingSoonTTDuration $eatingSoonTT $eatingSoonTT" + } + + else -> { + sendError(rh.gs(R.string.wear_action_tempt_preset_error, preset)) + return + } + } } else { - var low = SafeParse.stringToDouble(act[3]) - var high = SafeParse.stringToDouble(act[4]) - if (!isMGDL) { - low *= Constants.MMOLL_TO_MGDL - high *= Constants.MMOLL_TO_MGDL - } - if (low < HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[0] || low > HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[1]) { - sendError("Min-BG out of range!") + val isMGDL = java.lang.Boolean.parseBoolean(act[1]) + if (profileFunction.getUnits() == GlucoseUnit.MGDL != isMGDL) { + sendError(rh.gs(R.string.wear_action_tempt_unit_error)) return } - if (high < HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[0] || high > HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[1]) { - sendError("Max-BG out of range!") - return + val duration = SafeParse.stringToInt(act[2]) + if (duration == 0) { + rMessage += rh.gs(R.string.wear_action_tempt_zero_message) + rAction = "temptarget true 0 0 0" + } else { + var low = SafeParse.stringToDouble(act[3]) + var high = SafeParse.stringToDouble(act[4]) + if (!isMGDL) { + low *= Constants.MMOLL_TO_MGDL + high *= Constants.MMOLL_TO_MGDL + } + if (low < HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[0] || low > HardLimits.VERY_HARD_LIMIT_TEMP_MIN_BG[1]) { + sendError(rh.gs(R.string.wear_action_tempt_min_bg_error)) + return + } + if (high < HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[0] || high > HardLimits.VERY_HARD_LIMIT_TEMP_MAX_BG[1]) { + sendError(rh.gs(R.string.wear_action_tempt_max_bg_error)) + return + } + rMessage += if (act[3] === act[4]) rh.gs(R.string.wear_action_tempt_manual_message, act[3], act[2]) + else rh.gs(R.string.wear_action_tempt_manual_range_message, act[3], act[4], act[2]) + rAction = actionString } - rMessage += "Temptarget:\nMin: " + act[3] + "\nMax: " + act[4] + "\nDuration: " + act[2] - rAction = actionString } } else if ("status" == act[0]) { ////////////////////////////////////////////// STATUS rTitle = "STATUS" @@ -186,10 +236,15 @@ class ActionStringHandler @Inject constructor( sendError("Update APP on Watch!") return } else if ("wizard2" == act[0]) { ////////////////////////////////////////////// WIZARD + val pump = activePlugin.activePump + if (!pump.isInitialized() || pump.isSuspended() || loop.isDisconnected) { + sendError(rh.gs(R.string.wizard_pump_not_available)) + return + } val carbsBeforeConstraints = SafeParse.stringToInt(act[1]) val carbsAfterConstraints = constraintChecker.applyCarbsConstraints(Constraint(carbsBeforeConstraints)).value() if (carbsAfterConstraints - carbsBeforeConstraints != 0) { - sendError("Carb constraint violation!") + sendError(rh.gs(R.string.wizard_carbs_constraint)) return } val useBG = sp.getBoolean(R.string.key_wearwizard_bg, true) @@ -202,52 +257,94 @@ class ActionStringHandler @Inject constructor( val profile = profileFunction.getProfile() val profileName = profileFunction.getProfileName() if (profile == null) { - sendError("No profile found!") + sendError(rh.gs(R.string.wizard_no_active_profile)) return } val bgReading = iobCobCalculator.ads.actualBg() if (bgReading == null) { - sendError("No recent BG to base calculation on!") + sendError(rh.gs(R.string.wizard_no_actual_bg)) return } val cobInfo = iobCobCalculator.getCobInfo(false, "Wizard wear") if (cobInfo.displayCob == null) { - sendError("Unknown COB! BG reading missing or recent app restart?") + sendError(rh.gs(R.string.wizard_no_cob)) return } - val format = DecimalFormat("0.00") - val formatInt = DecimalFormat("0") val dbRecord = repository.getTemporaryTargetActiveAt(dateUtil.now()).blockingGet() val tempTarget = if (dbRecord is ValueWrapper.Existing) dbRecord.value else null - val bolusWizard = BolusWizard(injector).doCalc(profile, profileName, tempTarget, - carbsAfterConstraints, if (cobInfo.displayCob != null) cobInfo.displayCob!! else 0.0, bgReading.valueToUnits(profileFunction.getUnits()), - 0.0, percentage, useBG, useCOB, useBolusIOB, useBasalIOB, false, useTT, useTrend, false) - if (abs(bolusWizard.insulinAfterConstraints - bolusWizard.calculatedTotalInsulin) >= 0.01) { - sendError("Insulin constraint violation!" + - "\nCannot deliver " + format.format(bolusWizard.calculatedTotalInsulin) + "!") + val bolusWizard = BolusWizard(injector).doCalc( + profile, profileName, tempTarget, + carbsAfterConstraints, cobInfo.displayCob!!, bgReading.valueToUnits(profileFunction.getUnits()), + 0.0, percentage, useBG, useCOB, useBolusIOB, useBasalIOB, false, useTT, useTrend, false + ) + val insulinAfterConstraints = bolusWizard.insulinAfterConstraints + val minStep = pump.pumpDescription.pumpType.determineCorrectBolusStepSize(insulinAfterConstraints) + if (abs(insulinAfterConstraints - bolusWizard.calculatedTotalInsulin) >= minStep) { + sendError(rh.gs(R.string.wizard_constraint_bolus_size, bolusWizard.calculatedTotalInsulin)) return } if (bolusWizard.calculatedTotalInsulin <= 0 && bolusWizard.carbs <= 0) { rAction = "info" - rTitle = "INFO" + rTitle = rh.gs(R.string.info) } else { rAction = actionString } - rMessage += "Carbs: " + bolusWizard.carbs + "g" - rMessage += "\nBolus: " + format.format(bolusWizard.calculatedTotalInsulin) + "U" + rMessage += rh.gs(R.string.wizard_result, bolusWizard.calculatedTotalInsulin, bolusWizard.carbs) rMessage += "\n_____________" - rMessage += "\nCalc (IC:" + DecimalFormatter.to1Decimal(bolusWizard.ic) + ", " + "ISF:" + DecimalFormatter.to1Decimal(bolusWizard.sens) + "): " - rMessage += "\nFrom Carbs: " + format.format(bolusWizard.insulinFromCarbs) + "U" - if (useCOB) rMessage += "\nFrom" + formatInt.format(cobInfo.displayCob) + "g COB : " + format.format(bolusWizard.insulinFromCOB) + "U" - if (useBG) rMessage += "\nFrom BG: " + format.format(bolusWizard.insulinFromBG) + "U" - if (useBolusIOB) rMessage += "\nBolus IOB: " + format.format(bolusWizard.insulinFromBolusIOB) + "U" - if (useBasalIOB) rMessage += "\nBasal IOB: " + format.format(bolusWizard.insulinFromBasalIOB) + "U" - if (useTrend) rMessage += "\nFrom 15' trend: " + format.format(bolusWizard.insulinFromTrend) + "U" - if (percentage != 100) { - rMessage += "\nPercentage: " + format.format(bolusWizard.totalBeforePercentageAdjustment) + "U * " + percentage + "% -> ~" + format.format(bolusWizard.calculatedTotalInsulin) + "U" - } + rMessage += "\n" + bolusWizard.explainShort() lastBolusWizard = bolusWizard + } else if ("quick_wizard" == act[0]) { + val guid = act[1] + val actualBg = iobCobCalculator.ads.actualBg() + val profile = profileFunction.getProfile() + val profileName = profileFunction.getProfileName() + val quickWizardEntry = quickWizard.get(guid) + Log.i("QuickWizard", "handleInitiate: quick_wizard " + quickWizardEntry?.buttonText() + " c " + quickWizardEntry?.carbs()) + if (quickWizardEntry == null) { + sendError(rh.gs(R.string.quick_wizard_not_available)) + return + } + if (actualBg == null) { + sendError(rh.gs(R.string.wizard_no_actual_bg)) + return + } + if (profile == null) { + sendError(rh.gs(R.string.wizard_no_active_profile)) + return + } + val cobInfo = iobCobCalculator.getCobInfo(false, "QuickWizard wear") + if (cobInfo.displayCob == null) { + sendError(rh.gs(R.string.wizard_no_cob)) + return + } + val pump = activePlugin.activePump + if (!pump.isInitialized() || pump.isSuspended() || loop.isDisconnected) { + sendError(rh.gs(R.string.wizard_pump_not_available)) + return + } + + val wizard = quickWizardEntry.doCalc(profile, profileName, actualBg, true) + + val carbsAfterConstraints = constraintChecker.applyCarbsConstraints(Constraint(quickWizardEntry.carbs())).value() + if (carbsAfterConstraints != quickWizardEntry.carbs()) { + sendError(rh.gs(R.string.wizard_carbs_constraint)) + return + } + val insulinAfterConstraints = wizard.insulinAfterConstraints + val minStep = pump.pumpDescription.pumpType.determineCorrectBolusStepSize(insulinAfterConstraints) + if (abs(insulinAfterConstraints - wizard.calculatedTotalInsulin) >= minStep) { + sendError(rh.gs(R.string.wizard_constraint_bolus_size, wizard.calculatedTotalInsulin)) + return + } + + rMessage = rh.gs(R.string.quick_wizard_message, quickWizardEntry.buttonText(), wizard.calculatedTotalInsulin, quickWizardEntry.carbs()) + rAction = "bolus $insulinAfterConstraints $carbsAfterConstraints" + Log.i("QuickWizard", "handleInitiate: quick_wizard action=$rAction") + + rMessage += "\n_____________" + rMessage += "\n" + wizard.explainShort() + } else if ("opencpp" == act[0]) { val activeProfileSwitch = repository.getEffectiveProfileSwitchActiveAt(dateUtil.now()).blockingGet() if (activeProfileSwitch is ValueWrapper.Existing) { // read CPP values @@ -331,7 +428,10 @@ class ActionStringHandler @Inject constructor( rAction = "cancelChangeRequest" wearPlugin.requestNotificationCancel(rAction) return - } else return + } else { + sendError(rh.gs(R.string.wear_unknown_action_string) + act[0]) + return + } // send result wearPlugin.requestActionConfirmation(rTitle, rMessage, rAction) lastSentTimestamp = System.currentTimeMillis() @@ -560,39 +660,45 @@ class ActionStringHandler @Inject constructor( } //send profile to pump uel.log(Action.PROFILE_SWITCH, Sources.Wear, - ValueWithUnit.Percent(percentage), - ValueWithUnit.Hour(timeshift).takeIf { timeshift != 0 }) + ValueWithUnit.Percent(percentage), + ValueWithUnit.Hour(timeshift).takeIf { timeshift != 0 }) profileFunction.createProfileSwitch(0, percentage, timeshift) } private fun generateTempTarget(duration: Int, low: Double, high: Double) { if (duration != 0) { - disposable += repository.runTransactionForResult(InsertAndCancelCurrentTemporaryTargetTransaction( - timestamp = System.currentTimeMillis(), - duration = TimeUnit.MINUTES.toMillis(duration.toLong()), - reason = TemporaryTarget.Reason.WEAR, - lowTarget = Profile.toMgdl(low, profileFunction.getUnits()), - highTarget = Profile.toMgdl(high, profileFunction.getUnits()) - )).subscribe({ result -> - result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted temp target $it") } - result.updated.forEach { aapsLogger.debug(LTag.DATABASE, "Updated temp target $it") } - }, { - aapsLogger.error(LTag.DATABASE, "Error while saving temporary target", it) - }) - uel.log(Action.TT, Sources.Wear, + disposable += repository.runTransactionForResult( + InsertAndCancelCurrentTemporaryTargetTransaction( + timestamp = System.currentTimeMillis(), + duration = TimeUnit.MINUTES.toMillis(duration.toLong()), + reason = TemporaryTarget.Reason.WEAR, + lowTarget = Profile.toMgdl(low, profileFunction.getUnits()), + highTarget = Profile.toMgdl(high, profileFunction.getUnits()) + ) + ).subscribe({ result -> + result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted temp target $it") } + result.updated.forEach { aapsLogger.debug(LTag.DATABASE, "Updated temp target $it") } + }, { + aapsLogger.error(LTag.DATABASE, "Error while saving temporary target", it) + }) + uel.log( + Action.TT, Sources.Wear, ValueWithUnit.TherapyEventTTReason(TemporaryTarget.Reason.WEAR), ValueWithUnit.fromGlucoseUnit(low, profileFunction.getUnits().asText), ValueWithUnit.fromGlucoseUnit(high, profileFunction.getUnits().asText).takeIf { low != high }, - ValueWithUnit.Minute(duration)) + ValueWithUnit.Minute(duration) + ) } else { disposable += repository.runTransactionForResult(CancelCurrentTemporaryTargetIfAnyTransaction(System.currentTimeMillis())) .subscribe({ result -> - result.updated.forEach { aapsLogger.debug(LTag.DATABASE, "Updated temp target $it") } - }, { - aapsLogger.error(LTag.DATABASE, "Error while saving temporary target", it) - }) - uel.log(Action.CANCEL_TT, Sources.Wear, - ValueWithUnit.TherapyEventTTReason(TemporaryTarget.Reason.WEAR)) + result.updated.forEach { aapsLogger.debug(LTag.DATABASE, "Updated temp target $it") } + }, { + aapsLogger.error(LTag.DATABASE, "Error while saving temporary target", it) + }) + uel.log( + Action.CANCEL_TT, Sources.Wear, + ValueWithUnit.TherapyEventTTReason(TemporaryTarget.Reason.WEAR) + ) } } @@ -601,13 +707,15 @@ class ActionStringHandler @Inject constructor( detailedBolusInfo.insulin = amount detailedBolusInfo.bolusType = DetailedBolusInfo.BolusType.PRIMING uel.log(Action.PRIME_BOLUS, Sources.Wear, - ValueWithUnit.Insulin(amount).takeIf { amount != 0.0 }) + ValueWithUnit.Insulin(amount).takeIf { amount != 0.0 }) commandQueue.bolus(detailedBolusInfo, object : Callback() { override fun run() { if (!result.success) { - sendError(rh.gs(R.string.treatmentdeliveryerror) + - "\n" + - result.comment) + sendError( + rh.gs(R.string.treatmentdeliveryerror) + + "\n" + + result.comment + ) } } }) @@ -615,9 +723,9 @@ class ActionStringHandler @Inject constructor( private fun doECarbs(carbs: Int, time: Long, duration: Int) { uel.log(if (duration == 0) Action.CARBS else Action.EXTENDED_CARBS, Sources.Wear, - ValueWithUnit.Timestamp(time), - ValueWithUnit.Gram(carbs), - ValueWithUnit.Hour(duration).takeIf { duration != 0 }) + ValueWithUnit.Timestamp(time), + ValueWithUnit.Gram(carbs), + ValueWithUnit.Hour(duration).takeIf { duration != 0 }) doBolus(0.0, carbs, time, duration) } @@ -636,15 +744,17 @@ class ActionStringHandler @Inject constructor( else -> Action.TREATMENT } uel.log(action, Sources.Wear, - ValueWithUnit.Insulin(amount).takeIf { amount != 0.0 }, - ValueWithUnit.Gram(carbs).takeIf { carbs != 0 }, - ValueWithUnit.Hour(carbsDuration).takeIf { carbsDuration != 0 }) + ValueWithUnit.Insulin(amount).takeIf { amount != 0.0 }, + ValueWithUnit.Gram(carbs).takeIf { carbs != 0 }, + ValueWithUnit.Hour(carbsDuration).takeIf { carbsDuration != 0 }) commandQueue.bolus(detailedBolusInfo, object : Callback() { override fun run() { if (!result.success) { - sendError(rh.gs(R.string.treatmentdeliveryerror) + - "\n" + - result.comment) + sendError( + rh.gs(R.string.treatmentdeliveryerror) + + "\n" + + result.comment + ) } } }) @@ -665,4 +775,4 @@ class ActionStringHandler @Inject constructor( lastConfirmActionString = null lastBolusWizard = null } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java index 1ce5c509bb..95cf6128a2 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java @@ -46,6 +46,7 @@ import info.nightscout.androidaps.interfaces.Loop; import info.nightscout.androidaps.interfaces.PluginBase; import info.nightscout.androidaps.interfaces.Profile; import info.nightscout.androidaps.interfaces.ProfileFunction; +import info.nightscout.androidaps.utils.wizard.QuickWizardEntry; import info.nightscout.shared.logging.AAPSLogger; import info.nightscout.shared.logging.LTag; import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; @@ -62,6 +63,7 @@ import info.nightscout.androidaps.utils.DecimalFormatter; import info.nightscout.androidaps.utils.DefaultValueHelper; import info.nightscout.androidaps.utils.TrendCalculator; import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.wizard.QuickWizard; import info.nightscout.shared.sharedPreferences.SP; public class WatchUpdaterService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { @@ -81,6 +83,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog @Inject ReceiverStatusStore receiverStatusStore; @Inject Config config; @Inject public TrendCalculator trendCalculator; + @Inject public QuickWizard quickWizard; public static final String ACTION_RESEND = WatchUpdaterService.class.getName().concat(".Resend"); public static final String ACTION_OPEN_SETTINGS = WatchUpdaterService.class.getName().concat(".OpenSettings"); @@ -101,12 +104,14 @@ public class WatchUpdaterService extends WearableListenerService implements Goog private static final String OPEN_SETTINGS_PATH = "/openwearsettings"; private static final String NEW_STATUS_PATH = "/sendstatustowear"; private static final String NEW_PREFERENCES_PATH = "/sendpreferencestowear"; + private static final String QUICK_WIZARD_PATH = "/send_quick_wizard"; public static final String BASAL_DATA_PATH = "/nightscout_watch_basal"; public static final String BOLUS_PROGRESS_PATH = "/nightscout_watch_bolusprogress"; public static final String ACTION_CONFIRMATION_REQUEST_PATH = "/nightscout_watch_actionconfirmationrequest"; public static final String ACTION_CHANGECONFIRMATION_REQUEST_PATH = "/nightscout_watch_changeconfirmationrequest"; public static final String ACTION_CANCELNOTIFICATION_REQUEST_PATH = "/nightscout_watch_cancelnotificationrequest"; + String TAG = "WatchUpdateService"; private static boolean lastLoopStatus; @@ -156,7 +161,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog public int onStartCommand(Intent intent, int flags, int startId) { String action = intent != null ? intent.getAction() : null; - // Log.d(TAG, logPrefix + "onStartCommand: " + action); + // Log.d(TAG, "onStartCommand: " + action); if (wearIntegration()) { handler.post(() -> { @@ -235,7 +240,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog super.onPeerConnected(peer); String id = peer.getId(); String name = peer.getDisplayName(); - // Log.d(TAG, logPrefix + "onPeerConnected peer name & ID: " + name + "|" + id); + Log.d(TAG, "onPeerConnected peer name & ID: " + name + "|" + id); } @@ -244,14 +249,14 @@ public class WatchUpdaterService extends WearableListenerService implements Goog super.onPeerDisconnected(peer); String id = peer.getId(); String name = peer.getDisplayName(); - // Log.d(TAG, logPrefix + "onPeerDisconnected peer name & ID: " + name + "|" + id); + Log.d(TAG, "onPeerDisconnected peer name & ID: " + name + "|" + id); } @Override public void onMessageReceived(MessageEvent event) { - // Log.d(TAG, logPrefix + "onMessageRecieved: " + event); + // Log.d(TAG, "onMessageRecieved: " + event); if (wearIntegration()) { if (event != null && event.getPath().equals(WEARABLE_RESEND_PATH)) { @@ -283,7 +288,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog private void sendData() { GlucoseValue lastBG = iobCobCalculator.getAds().lastBg(); - // Log.d(TAG, logPrefix + "LastBg=" + lastBG); + // Log.d(TAG, "LastBg=" + lastBG); if (lastBG != null) { GlucoseStatus glucoseStatus = glucoseStatusProvider.getGlucoseStatusData(); @@ -364,6 +369,10 @@ public class WatchUpdaterService extends WearableListenerService implements Goog if (googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) { googleApiConnect(); } + + sendPreferences(); + sendQuickWizard(); + long startTime = System.currentTimeMillis() - (long) (60000 * 60 * 5.5); GlucoseValue last_bg = iobCobCalculator.getAds().lastBg(); @@ -382,7 +391,6 @@ public class WatchUpdaterService extends WearableListenerService implements Goog entries.putDataMapArrayList("entries", dataMaps); (new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, entries); } - sendPreferences(); sendBasals(); sendStatus(); } @@ -720,19 +728,62 @@ public class WatchUpdaterService extends WearableListenerService implements Goog private void sendPreferences() { if (googleApiClient != null && googleApiClient.isConnected()) { + GlucoseUnit units = profileFunction.getUnits(); boolean wearcontrol = sp.getBoolean(R.string.key_wear_control, false); - + boolean mgdl = units.equals(GlucoseUnit.MGDL); + int percentage = sp.getInt(R.string.key_boluswizard_percentage, 100); + int maxCarbs = sp.getInt(R.string.key_treatmentssafety_maxcarbs, 48); + double maxBolus = sp.getDouble(R.string.key_treatmentssafety_maxbolus, 3.0); PutDataMapRequest dataMapRequest = PutDataMapRequest.create(NEW_PREFERENCES_PATH); //unique content dataMapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis()); dataMapRequest.getDataMap().putBoolean(rh.gs(R.string.key_wear_control), wearcontrol); + dataMapRequest.getDataMap().putBoolean(rh.gs(R.string.key_units_mgdl), mgdl); + dataMapRequest.getDataMap().putInt(rh.gs(R.string.key_boluswizard_percentage), percentage); + dataMapRequest.getDataMap().putInt(rh.gs(R.string.key_treatmentssafety_maxcarbs), maxCarbs); + dataMapRequest.getDataMap().putDouble(rh.gs(R.string.key_treatmentssafety_maxbolus),maxBolus); PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); } else { - Log.e("SendStatus", "No connection to wearable available!"); + Log.e("SendPreferences", "No connection to wearable available!"); } } + private void sendQuickWizard() { + if (googleApiClient != null && googleApiClient.isConnected()) { + int size = quickWizard.size(); + ArrayList entities = new ArrayList<>(); + for(int i=0; i < size; i++) { + QuickWizardEntry q = quickWizard.get(i); + if (q.forDevice(QuickWizardEntry.DEVICE_WATCH)) { + entities.add(quickMap(q)); + } + } + + PutDataMapRequest dataMapRequest = PutDataMapRequest.create(QUICK_WIZARD_PATH); + + DataMap dm = dataMapRequest.getDataMap(); + dm.putLong("timestamp", System.currentTimeMillis()); + dm.putDataMapArrayList("quick_wizard", entities); + + PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + Log.i(TAG, "sendQuickWizard: " + putDataRequest); + Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); + } else { + Log.e("sendQuickWizard", "No connection to wearable available!"); + } + } + + private DataMap quickMap(QuickWizardEntry q) { + DataMap dm = new DataMap(); + dm.putString("guid", q.guid()); + dm.putString("button_text", q.buttonText()); + dm.putInt("carbs", q.carbs()); + dm.putInt("from", q.validFrom()); + dm.putInt("to", q.validTo()); + return dm; + } + @NonNull private String generateStatusString(Profile profile, String currentBasal, String iobSum, String iobDetail, String bgiString) { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/profile/local/LocalProfileFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/profile/local/LocalProfileFragment.kt index 8892c397a9..1fa1f3711f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/profile/local/LocalProfileFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/profile/local/LocalProfileFragment.kt @@ -121,6 +121,7 @@ class LocalProfileFragment : DaggerFragment() { processVisibilityOnClick(it) binding.target.visibility = View.VISIBLE } + binding.dia.editText?.id?.let { binding.diaLabel.labelFor = it } } fun build() { diff --git a/app/src/main/java/info/nightscout/androidaps/utils/stats/TddCalculator.kt b/app/src/main/java/info/nightscout/androidaps/utils/stats/TddCalculator.kt index 88d281a348..0f08ccea8a 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/stats/TddCalculator.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/stats/TddCalculator.kt @@ -58,13 +58,13 @@ class TddCalculator @Inject constructor( val tbr = tempBasals[t] val profile = profileFunction.getProfile(t) ?: continue val absoluteRate = tbr?.convertedToAbsolute(t, profile) ?: profile.getBasal(t) - tdd.basalAmount += absoluteRate / 60.0 * 5.0 + tdd.basalAmount += absoluteRate / T.mins(60).msecs().toDouble() * calculationStep.toDouble() if (!activePlugin.activePump.isFakingTempsByExtendedBoluses) { // they are not included in TBRs val eb = iobCobCalculator.getExtendedBolus(t) val absoluteEbRate = eb?.rate ?: 0.0 - tdd.bolusAmount += absoluteEbRate / 60.0 * 5.0 + tdd.bolusAmount += absoluteEbRate / T.mins(60).msecs().toDouble() * calculationStep.toDouble() } result.put(midnight, tdd) } @@ -76,7 +76,54 @@ class TddCalculator @Inject constructor( return result } - private fun averageTDD(tdds: LongSparseArray): TotalDailyDose { + fun calculateDaily():TotalDailyDose { + val startTime = MidnightTime.calc(dateUtil.now() ) + val endTime = dateUtil.now() + val tdd = TotalDailyDose(timestamp = startTime) + //val result = TotalDailyDose() + repository.getBolusesDataFromTimeToTime(startTime, endTime, true).blockingGet() + .filter { it.type != Bolus.Type.PRIMING } + .forEach { t -> + //val midnight = MidnightTime.calc(t.timestamp) + //val tdd = result[midnight] ?: TotalDailyDose(timestamp = midnight) + tdd.bolusAmount += t.amount + //result.put(midnight, tdd) + } + repository.getCarbsDataFromTimeToTimeExpanded(startTime, endTime, true).blockingGet().forEach { t -> + //val midnight = MidnightTime.calc(t.timestamp) + //val tdd = result[midnight] ?: TotalDailyDose(timestamp = midnight) + tdd.carbs += t.amount + //result.put(midnight, tdd) + } + val calculationStep = T.mins(5).msecs() + for (t in startTime until endTime step calculationStep) { + + //val midnight = MidnightTime.calc(t) + //val tdd = result[midnight] ?: TotalDailyDose(timestamp = midnight) + val tbr = iobCobCalculator.getTempBasalIncludingConvertedExtended(t) + val profile = profileFunction.getProfile(t) ?: continue + val absoluteRate = tbr?.convertedToAbsolute(t, profile) ?: profile.getBasal(t) + tdd.basalAmount += absoluteRate / T.mins(5).msecs().toDouble() * calculationStep.toDouble() + + if (!activePlugin.activePump.isFakingTempsByExtendedBoluses) { + // they are not included in TBRs + val eb = iobCobCalculator.getExtendedBolus(t) + val absoluteEbRate = eb?.rate ?: 0.0 + tdd.bolusAmount += absoluteEbRate / T.mins(5).msecs().toDouble() * calculationStep.toDouble() + } + //result.put(midnight, tdd) + } + //for (i in 0 until tdd.size()) { + //val tdd = result.valueAt(i) + tdd.totalAmount = tdd.bolusAmount + tdd.basalAmount + //} + + + aapsLogger.debug(LTag.CORE, tdd.toString()) + return tdd + } + + fun averageTDD(tdds: LongSparseArray): TotalDailyDose { val totalTdd = TotalDailyDose(timestamp = dateUtil.now()) for (i in 0 until tdds.size()) { val tdd = tdds.valueAt(i) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/ui/TimeListEdit.java b/app/src/main/java/info/nightscout/androidaps/utils/ui/TimeListEdit.java index 918758fce4..1d79a9205e 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/ui/TimeListEdit.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/ui/TimeListEdit.java @@ -112,7 +112,7 @@ public class TimeListEdit { float factor = layout.getContext().getResources().getDisplayMetrics().density; finalAdd = new ImageView(context); finalAdd.setImageResource(R.drawable.ic_add); - finalAdd.setContentDescription(layout.getContext().getResources().getString(R.string.addnew)); + finalAdd.setContentDescription(layout.getContext().getResources().getString(R.string.a11y_add_new_to_list)); LinearLayout.LayoutParams illp = new LinearLayout.LayoutParams((int) (35d * factor), (int) (35 * factor)); illp.setMargins(0, 25, 0, 25); // llp.setMargins(left, top, right, bottom); illp.gravity = Gravity.CENTER; diff --git a/app/src/main/java/info/nightscout/androidaps/utils/wizard/BolusWizard.kt b/app/src/main/java/info/nightscout/androidaps/utils/wizard/BolusWizard.kt index 5a66cd2510..de30b2f565 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/wizard/BolusWizard.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/wizard/BolusWizard.kt @@ -18,6 +18,8 @@ import info.nightscout.androidaps.database.entities.ValueWithUnit import info.nightscout.androidaps.database.transactions.InsertOrUpdateBolusCalculatorResultTransaction import info.nightscout.androidaps.events.EventRefreshOverview import info.nightscout.androidaps.extensions.formatColor +import info.nightscout.androidaps.extensions.highValueToUnitsToString +import info.nightscout.androidaps.extensions.lowValueToUnitsToString import info.nightscout.androidaps.interfaces.* import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.logging.LTag @@ -135,27 +137,28 @@ class BolusWizard @Inject constructor( private var quickWizard: Boolean = true var usePercentage: Boolean = false - fun doCalc(profile: Profile, - profileName: String, - tempTarget: TemporaryTarget?, - carbs: Int, - cob: Double, - bg: Double, - correction: Double, - percentageCorrection: Int = 100, - useBg: Boolean, - useCob: Boolean, - includeBolusIOB: Boolean, - includeBasalIOB: Boolean, - useSuperBolus: Boolean, - useTT: Boolean, - useTrend: Boolean, - useAlarm: Boolean, - notes: String = "", - carbTime: Int = 0, - usePercentage: Boolean = false, - totalPercentage: Double = 100.0, - quickWizard: Boolean = false + fun doCalc( + profile: Profile, + profileName: String, + tempTarget: TemporaryTarget?, + carbs: Int, + cob: Double, + bg: Double, + correction: Double, + percentageCorrection: Int = 100, + useBg: Boolean, + useCob: Boolean, + includeBolusIOB: Boolean, + includeBasalIOB: Boolean, + useSuperBolus: Boolean, + useTT: Boolean, + useTrend: Boolean, + useAlarm: Boolean, + notes: String = "", + carbTime: Int = 0, + usePercentage: Boolean = false, + totalPercentage: Double = 100.0, + quickWizard: Boolean = false ): BolusWizard { this.profile = profile @@ -314,7 +317,9 @@ class BolusWizard @Inject constructor( actions.add(rh.gs(R.string.carbs) + ": " + rh.gs(R.string.format_carbs, carbs).formatColor(rh, R.color.carbs) + timeShift) } if (insulinFromCOB > 0) { - actions.add(rh.gs(R.string.cobvsiob) + ": " + rh.gs(R.string.formatsignedinsulinunits, insulinFromBolusIOB + insulinFromBasalIOB + insulinFromCOB + insulinFromBG).formatColor(rh, R.color.cobAlert)) + actions.add( + rh.gs(R.string.cobvsiob) + ": " + rh.gs(R.string.formatsignedinsulinunits, insulinFromBolusIOB + insulinFromBasalIOB + insulinFromCOB + insulinFromBG).formatColor(rh, R.color.cobAlert) + ) val absorptionRate = iobCobCalculator.ads.slowAbsorptionPercentage(60) if (absorptionRate > .25) actions.add(rh.gs(R.string.slowabsorptiondetected, rh.gc(R.color.cobAlert), (absorptionRate * 100).toInt())) @@ -344,8 +349,8 @@ class BolusWizard @Inject constructor( carbTimer.removeEatReminder() if (sp.getBoolean(R.string.key_usebolusadvisor, false) && Profile.toMgdl(bg, profile.units) > 180 && carbs > 0 && carbTime >= 0) OKDialog.showYesNoCancel(ctx, rh.gs(R.string.bolusadvisor), rh.gs(R.string.bolusadvisormessage), - { bolusAdvisorProcessing(ctx) }, - { commonProcessing(ctx) } + { bolusAdvisorProcessing(ctx) }, + { commonProcessing(ctx) } ) else commonProcessing(ctx) @@ -367,10 +372,13 @@ class BolusWizard @Inject constructor( carbTime = 0 bolusCalculatorResult = createBolusCalculatorResult() notes = this@BolusWizard.notes - uel.log(Action.BOLUS_ADVISOR, if (quickWizard) Sources.QuickWizard else Sources.WizardDialog, + uel.log( + Action.BOLUS_ADVISOR, + if (quickWizard) Sources.QuickWizard else Sources.WizardDialog, notes, ValueWithUnit.TherapyEventType(eventType.toDBbEventType()), - ValueWithUnit.Insulin(insulinAfterConstraints)) + ValueWithUnit.Insulin(insulinAfterConstraints) + ) if (insulin > 0) { commandQueue.bolus(this, object : Callback() { override fun run() { @@ -385,6 +393,26 @@ class BolusWizard @Inject constructor( }) } + fun explainShort(): String { + var message = rh.gs(R.string.wizard_explain_calc, ic, sens) + message += "\n" + rh.gs(R.string.wizard_explain_carbs, insulinFromCarbs) + if (useTT && tempTarget != null) { + val tt = if (tempTarget?.lowTarget == tempTarget?.highTarget) tempTarget?.lowValueToUnitsToString(profile.units) + else rh.gs(R.string.wizard_explain_tt_to, tempTarget?.lowValueToUnitsToString(profile.units), tempTarget?.highValueToUnitsToString(profile.units)) + message += "\n" + rh.gs(R.string.wizard_explain_tt, tt) + } + if (useCob) message += "\n" + rh.gs(R.string.wizard_explain_cob, cob, insulinFromCOB) + if (useBg) message += "\n" + rh.gs(R.string.wizard_explain_bg, insulinFromBG) + if (includeBolusIOB) message += "\n" + rh.gs(R.string.wizard_explain_bolus_iob, insulinFromBolusIOB) + if (includeBasalIOB) message += "\n" + rh.gs(R.string.wizard_explain_basal_iob, insulinFromBasalIOB) + if (useTrend) message += "\n" + rh.gs(R.string.wizard_explain_trend, insulinFromTrend) + if (useSuperBolus) message += "\n" + rh.gs(R.string.wizard_explain_superbolus, insulinFromSuperBolus) + if (percentageCorrection != 100) { + message += "\n" + rh.gs(R.string.wizard_explain_percent, totalBeforePercentageAdjustment, percentageCorrection, calculatedTotalInsulin) + } + return message + } + private fun commonProcessing(ctx: Context) { val profile = profileFunction.getProfile() ?: return val pump = activePlugin.activePump @@ -433,17 +461,17 @@ class BolusWizard @Inject constructor( bolusCalculatorResult = createBolusCalculatorResult() notes = this@BolusWizard.notes if (insulin > 0 || carbs > 0) { - val action = when { + val action = when { insulinAfterConstraints.equals(0.0) -> Action.CARBS carbs.equals(0.0) -> Action.BOLUS else -> Action.TREATMENT } uel.log(action, if (quickWizard) Sources.QuickWizard else Sources.WizardDialog, - notes, - ValueWithUnit.TherapyEventType(eventType.toDBbEventType()), - ValueWithUnit.Insulin(insulinAfterConstraints).takeIf { insulinAfterConstraints != 0.0 }, - ValueWithUnit.Gram(this@BolusWizard.carbs).takeIf { this@BolusWizard.carbs != 0 }, - ValueWithUnit.Minute(carbTime).takeIf { carbTime != 0 }) + notes, + ValueWithUnit.TherapyEventType(eventType.toDBbEventType()), + ValueWithUnit.Insulin(insulinAfterConstraints).takeIf { insulinAfterConstraints != 0.0 }, + ValueWithUnit.Gram(this@BolusWizard.carbs).takeIf { this@BolusWizard.carbs != 0 }, + ValueWithUnit.Minute(carbTime).takeIf { carbTime != 0 }) commandQueue.bolus(this, object : Callback() { override fun run() { if (!result.success) { @@ -469,9 +497,9 @@ class BolusWizard @Inject constructor( private fun calcPercentageWithConstraints() { calculatedPercentage = 100.0 if (totalBeforePercentageAdjustment != insulinFromCorrection) - calculatedPercentage = calculatedTotalInsulin/(totalBeforePercentageAdjustment-insulinFromCorrection) * 100 + calculatedPercentage = calculatedTotalInsulin / (totalBeforePercentageAdjustment - insulinFromCorrection) * 100 calculatedPercentage = max(calculatedPercentage, 10.0) - calculatedPercentage = min(calculatedPercentage,250.0) + calculatedPercentage = min(calculatedPercentage, 250.0) } private fun calcCorrectionWithConstraints() { @@ -481,4 +509,4 @@ class BolusWizard @Inject constructor( calculatedCorrection = max(-constraintChecker.getMaxBolusAllowed().value(), calculatedCorrection) } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt index a254831690..b48d44c9cc 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt @@ -1,10 +1,12 @@ package info.nightscout.androidaps.utils.wizard +import android.util.Log import dagger.android.HasAndroidInjector import info.nightscout.androidaps.R import info.nightscout.shared.sharedPreferences.SP import org.json.JSONArray import org.json.JSONObject +import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -18,6 +20,18 @@ class QuickWizard @Inject constructor( init { setData(JSONArray(sp.getString(R.string.key_quickwizard, "[]"))) + setGuidsForOldEntries() + } + + private fun setGuidsForOldEntries() { + // for migration purposes; guid is a new required property + for (i in 0 until storage.length()) { + val entry = QuickWizardEntry(injector).from(storage.get(i) as JSONObject, i) + if (entry.guid() == "") { + val guid = UUID.randomUUID().toString() + entry.storage.put("guid", guid) + } + } } fun getActive(): QuickWizardEntry? { @@ -41,6 +55,38 @@ class QuickWizard @Inject constructor( operator fun get(position: Int): QuickWizardEntry = QuickWizardEntry(injector).from(storage.get(position) as JSONObject, position) + fun get(guid: String): QuickWizardEntry? { + for (i in 0 until storage.length()) { + val entry = QuickWizardEntry(injector).from(storage.get(i) as JSONObject, i) + if (entry.guid() == guid) { + return entry + } + } + return null + } + + fun move(from: Int, to: Int) { + Log.i("QuickWizard", "moveItem: $from $to") + val fromEntry = storage[from] as JSONObject + storage.remove(from) + addToPos(to, fromEntry, storage) + save() + } + + fun removePos(pos: Int, jsonObj: JSONObject?, jsonArr: JSONArray) { + for (i in jsonArr.length() downTo pos + 1) { + jsonArr.put(i, jsonArr[i - 1]) + } + jsonArr.put(pos, jsonObj) + } + + private fun addToPos(pos: Int, jsonObj: JSONObject?, jsonArr: JSONArray) { + for (i in jsonArr.length() downTo pos + 1) { + jsonArr.put(i, jsonArr[i - 1]) + } + jsonArr.put(pos, jsonObj) + } + fun newEmptyItem(): QuickWizardEntry { return QuickWizardEntry(injector) } @@ -57,4 +103,5 @@ class QuickWizard @Inject constructor( storage.remove(position) save() } + } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt index 79ff43bf66..c6dee8a660 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt @@ -19,6 +19,7 @@ import info.nightscout.androidaps.utils.JsonHelper.safeGetString import info.nightscout.shared.sharedPreferences.SP import org.json.JSONException import org.json.JSONObject +import java.util.* import javax.inject.Inject class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjector) { @@ -41,11 +42,26 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec const val NO = 1 private const val POSITIVE_ONLY = 2 private const val NEGATIVE_ONLY = 3 + const val DEVICE_ALL = 0 + const val DEVICE_PHONE = 1 + const val DEVICE_WATCH = 2 + const val DEFAULT = 0 + const val CUSTOM = 1 } init { injector.androidInjector().inject(this) - val emptyData = "{\"buttonText\":\"\",\"carbs\":0,\"validFrom\":0,\"validTo\":86340}" + val guid = UUID.randomUUID().toString() + val emptyData = """{ + "guid": "$guid", + "buttonText": "", + "carbs": 0, + "validFrom": 0, + "validTo": 86340, + "device": "all", + "usePercentage": "default", + "percentage": 100 + }""".trimMargin() try { storage = JSONObject(emptyData) } catch (e: JSONException) { @@ -55,6 +71,8 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec /* { + guid: string, + device: string, // (phone, watch, all) buttonText: "Meal", carbs: 36, validFrom: 8 * 60 * 60, // seconds from midnight @@ -66,15 +84,18 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec useTrend: 0, useSuperBolus: 0, useTemptarget: 0 + usePercentage: string, // default, custom + percentage: int, } */ fun from(entry: JSONObject, position: Int): QuickWizardEntry { + // TODO set guid if missing for migration storage = entry this.position = position return this } - fun isActive(): Boolean = profileFunction.secondsFromMidnight() >= validFrom() && profileFunction.secondsFromMidnight() <= validTo() + fun isActive(): Boolean = profileFunction.secondsFromMidnight() >= validFrom() && profileFunction.secondsFromMidnight() <= validTo() && forDevice(DEVICE_PHONE) fun doCalc(profile: Profile, profileName: String, lastBG: GlucoseValue, _synchronized: Boolean): BolusWizard { val dbRecord = repository.getTemporaryTargetActiveAt(dateUtil.now()).blockingGet() @@ -119,10 +140,16 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec } else if (useTrend() == NEGATIVE_ONLY && glucoseStatus != null && glucoseStatus.shortAvgDelta < 0) { trend = true } - val percentage = sp.getInt(R.string.key_boluswizard_percentage, 100) + val percentage = if (usePercentage() == DEFAULT) sp.getInt(R.string.key_boluswizard_percentage, 100) else percentage() return BolusWizard(injector).doCalc(profile, profileName, tempTarget, carbs(), cob, bg, 0.0, percentage, true, useCOB() == YES, bolusIOB, basalIOB, superBolus, useTempTarget() == YES, trend, false, buttonText(), quickWizard = true) //tbc, ok if only quickwizard, but if other sources elsewhere use Sources.QuickWizard } + fun guid(): String = safeGetString(storage, "guid", "") + + fun device(): Int = safeGetInt(storage, "device", DEVICE_ALL) + + fun forDevice(device: Int) = device() == device || device() == DEVICE_ALL + fun buttonText(): String = safeGetString(storage, "buttonText", "") fun carbs(): Int = safeGetInt(storage, "carbs") @@ -148,4 +175,8 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec fun useSuperBolus(): Int = safeGetInt(storage, "useSuperBolus", NO) fun useTempTarget(): Int = safeGetInt(storage, "useTempTarget", NO) -} \ No newline at end of file + + fun usePercentage(): Int = safeGetInt(storage, "usePercentage", DEFAULT) + + fun percentage(): Int = safeGetInt(storage, "percentage", 100) +} diff --git a/app/src/main/res/layout/activity_profilehelper.xml b/app/src/main/res/layout/activity_profilehelper.xml index 224df803f1..d87aca5296 100644 --- a/app/src/main/res/layout/activity_profilehelper.xml +++ b/app/src/main/res/layout/activity_profilehelper.xml @@ -83,14 +83,14 @@ android:layout_width="150dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:labelFor="@+id/age" android:text="@string/age" android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> + android:layout_height="40dp" + app:customContentDescription="@string/age" /> @@ -105,14 +105,14 @@ android:layout_width="150dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:labelFor="@+id/weight" android:text="@string/tdd_total" android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> + android:layout_height="40dp" + app:customContentDescription="@string/tdd_total" /> @@ -123,18 +123,18 @@ android:gravity="center_vertical"> + android:layout_height="40dp" + app:customContentDescription="@string/weight_label" /> @@ -149,14 +149,14 @@ android:layout_width="150dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:labelFor="@+id/basalpctfromtdd" android:text="@string/basalpctfromtdd_label" android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> + android:layout_height="40dp" + app:customContentDescription="@string/basalpctfromtdd_label" /> diff --git a/app/src/main/res/layout/dialog_calibration.xml b/app/src/main/res/layout/dialog_calibration.xml index 5190ecc325..9698f5311e 100644 --- a/app/src/main/res/layout/dialog_calibration.xml +++ b/app/src/main/res/layout/dialog_calibration.xml @@ -58,6 +58,7 @@ android:paddingTop="10dp"> + android:layout_height="40dp" + app:customContentDescription="@string/treatments_wizard_bg_label" /> - - + android:padding="2dp" + android:layoutDirection="rtl" + android:contentDescription="@string/a11y_carb_reminder" + android:drawableEnd="@drawable/ic_access_alarm_24dp" /> + + android:layout_height="40dp" + app:customContentDescription="@string/time_offset" /> + android:layout_gravity="center_horizontal" + app:customContentDescription="@string/careportal_newnstreatment_duration_label" /> + android:layout_height="40dp" + app:customContentDescription="@string/treatments_wizard_carbs_label" /> + android:layout_height="40dp" + app:customContentDescription="@string/treatments_wizard_bg_label" /> + android:layout_height="40dp" + app:customContentDescription="@string/careportal_newnstreatment_duration_label" /> + android:layout_height="40dp" + app:customContentDescription="@string/overview_insulin_label" /> + android:layout_height="40dp" + app:customContentDescription="@string/careportal_newnstreatment_duration_label" /> + android:paddingRight="5dp" + app:customContentDescription="@string/overview_insulin_label" /> + android:layout_height="40dp" + app:customContentDescription="@string/time_offset"/> + android:layout_height="40dp" + app:customContentDescription="@string/overview_insulin_label"/>