Add Autotune Plugin
This commit is contained in:
parent
b1165caef3
commit
3d02c7f57c
23 changed files with 3480 additions and 2 deletions
|
@ -28,6 +28,7 @@ import info.nightscout.androidaps.plugins.bus.RxBus
|
|||
import info.nightscout.androidaps.plugins.configBuilder.PluginStore
|
||||
import info.nightscout.androidaps.plugins.constraints.safety.SafetyPlugin
|
||||
import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotunePlugin
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus
|
||||
|
@ -65,6 +66,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
|
|||
@Inject lateinit var config: Config
|
||||
|
||||
@Inject lateinit var automationPlugin: AutomationPlugin
|
||||
@Inject lateinit var autotunePlugin: AutotunePlugin
|
||||
@Inject lateinit var danaRPlugin: DanaRPlugin
|
||||
@Inject lateinit var danaRKoreanPlugin: DanaRKoreanPlugin
|
||||
@Inject lateinit var danaRv2Plugin: DanaRv2Plugin
|
||||
|
@ -187,6 +189,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
|
|||
addPreferencesFromResourceIfEnabled(tidepoolPlugin, rootKey)
|
||||
addPreferencesFromResourceIfEnabled(smsCommunicatorPlugin, rootKey)
|
||||
addPreferencesFromResourceIfEnabled(automationPlugin, rootKey)
|
||||
addPreferencesFromResourceIfEnabled(autotunePlugin, rootKey)
|
||||
addPreferencesFromResourceIfEnabled(wearPlugin, rootKey)
|
||||
addPreferencesFromResourceIfEnabled(statusLinePlugin, rootKey)
|
||||
addPreferencesFromResource(R.xml.pref_alerts, rootKey)
|
||||
|
|
|
@ -12,6 +12,7 @@ import info.nightscout.androidaps.dana.di.DanaModule
|
|||
import info.nightscout.androidaps.danar.di.DanaRModule
|
||||
import info.nightscout.androidaps.danars.di.DanaRSModule
|
||||
import info.nightscout.androidaps.database.DatabaseModule
|
||||
import info.nightscout.androidaps.dependencyInjection.AutotuneModule
|
||||
import info.nightscout.androidaps.diaconn.di.DiaconnG8Module
|
||||
import info.nightscout.androidaps.insight.di.InsightDatabaseModule
|
||||
import info.nightscout.androidaps.insight.di.InsightModule
|
||||
|
@ -37,6 +38,7 @@ import javax.inject.Singleton
|
|||
ReceiversModule::class,
|
||||
ServicesModule::class,
|
||||
AutomationModule::class,
|
||||
AutotuneModule::class,
|
||||
CommandQueueModule::class,
|
||||
ObjectivesModule::class,
|
||||
WizardModule::class,
|
||||
|
|
|
@ -15,6 +15,7 @@ import info.nightscout.androidaps.plugins.bus.RxBus
|
|||
import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin
|
||||
import info.nightscout.androidaps.plugins.configBuilder.PluginStore
|
||||
import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctionImplementation
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotunePlugin
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefsImpl
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.PrefFileListProvider
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.DataSyncSelectorImplementation
|
||||
|
@ -98,6 +99,7 @@ open class AppModule {
|
|||
@Binds fun bindImportExportPrefsInterface(importExportPrefs: ImportExportPrefsImpl): ImportExportPrefs
|
||||
@Binds fun bindIconsProviderInterface(iconsProvider: IconsProviderImplementation): IconsProvider
|
||||
@Binds fun bindLoopInterface(loopPlugin: LoopPlugin): Loop
|
||||
@Binds fun bindAutotuneInterface(autotunePlugin: AutotunePlugin): Autotune
|
||||
@Binds fun bindIobCobCalculatorInterface(iobCobCalculatorPlugin: IobCobCalculatorPlugin): IobCobCalculator
|
||||
@Binds fun bindSmsCommunicatorInterface(smsCommunicatorPlugin: SmsCommunicatorPlugin): SmsCommunicator
|
||||
@Binds fun bindDataSyncSelector(dataSyncSelectorImplementation: DataSyncSelectorImplementation): DataSyncSelector
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package info.nightscout.androidaps.dependencyInjection
|
||||
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotuneCore
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotuneIob
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotunePrep
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotuneFS
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.*
|
||||
|
||||
@Module
|
||||
@Suppress("unused")
|
||||
abstract class AutotuneModule {
|
||||
@ContributesAndroidInjector abstract fun autoTunePrepInjector(): AutotunePrep
|
||||
@ContributesAndroidInjector abstract fun autoTuneIobInjector(): AutotuneIob
|
||||
@ContributesAndroidInjector abstract fun autoTuneCoreInjector(): AutotuneCore
|
||||
@ContributesAndroidInjector abstract fun autoTuneFSInjector(): AutotuneFS
|
||||
|
||||
@ContributesAndroidInjector abstract fun autoTuneATProfileInjector(): ATProfile
|
||||
@ContributesAndroidInjector abstract fun autoTuneBGDatumInjector(): BGDatum
|
||||
@ContributesAndroidInjector abstract fun autoTuneCRDatumInjector(): CRDatum
|
||||
@ContributesAndroidInjector abstract fun autoTunePreppedGlucoseInjector(): PreppedGlucose
|
||||
}
|
|
@ -12,6 +12,7 @@ import info.nightscout.androidaps.plugins.constraints.objectives.ObjectivesFragm
|
|||
import info.nightscout.androidaps.plugins.constraints.objectives.activities.ObjectivesExamDialog
|
||||
import info.nightscout.androidaps.plugins.general.actions.ActionsFragment
|
||||
import info.nightscout.androidaps.plugins.general.automation.AutomationFragment
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotuneFragment
|
||||
import info.nightscout.androidaps.plugins.general.food.FoodFragment
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.MaintenanceFragment
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.NSClientFragment
|
||||
|
@ -36,6 +37,7 @@ abstract class FragmentsModule {
|
|||
|
||||
@ContributesAndroidInjector abstract fun contributesActionsFragment(): ActionsFragment
|
||||
@ContributesAndroidInjector abstract fun contributesAutomationFragment(): AutomationFragment
|
||||
@ContributesAndroidInjector abstract fun contributesAutotuneFragment(): AutotuneFragment
|
||||
@ContributesAndroidInjector abstract fun contributesBGSourceFragment(): BGSourceFragment
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesConfigBuilderFragment(): ConfigBuilderFragment
|
||||
|
|
|
@ -25,6 +25,7 @@ import info.nightscout.androidaps.plugins.constraints.storage.StorageConstraintP
|
|||
import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerPlugin
|
||||
import info.nightscout.androidaps.plugins.general.actions.ActionsPlugin
|
||||
import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin
|
||||
import info.nightscout.androidaps.plugins.general.autotune.AutotunePlugin
|
||||
import info.nightscout.androidaps.plugins.general.dataBroadcaster.DataBroadcastPlugin
|
||||
import info.nightscout.androidaps.plugins.general.food.FoodPlugin
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin
|
||||
|
@ -232,6 +233,12 @@ abstract class PluginsModule {
|
|||
@IntKey(250)
|
||||
abstract fun bindAutomationPlugin(plugin: AutomationPlugin): PluginBase
|
||||
|
||||
@Binds
|
||||
@AllConfigs
|
||||
@IntoMap
|
||||
@IntKey(255)
|
||||
abstract fun bindAutotunePlugin(plugin: AutotunePlugin): PluginBase
|
||||
|
||||
@Binds
|
||||
@AllConfigs
|
||||
@IntoMap
|
||||
|
|
|
@ -0,0 +1,514 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune
|
||||
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin
|
||||
import info.nightscout.androidaps.utils.Round
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AutotuneCore @Inject constructor(
|
||||
private val sp: SP,
|
||||
private val autotuneFS: AutotuneFS
|
||||
) {
|
||||
|
||||
fun tuneAllTheThings(preppedGlucose: PreppedGlucose, previousAutotune: ATProfile, pumpProfile: ATProfile): ATProfile {
|
||||
//var pumpBasalProfile = pumpProfile.basalprofile;
|
||||
val pumpBasalProfile = pumpProfile.basal
|
||||
//console.error(pumpBasalProfile);
|
||||
var basalProfile = previousAutotune.basal
|
||||
//console.error(basalProfile);
|
||||
//console.error(isfProfile);
|
||||
var isf = previousAutotune.isf
|
||||
//console.error(isf);
|
||||
var carbRatio = previousAutotune.ic
|
||||
//console.error(carbRatio);
|
||||
var csf = isf / carbRatio
|
||||
//val dia = previousAutotune.dia
|
||||
//val insulinInterface = activePlugin.activeInsulin
|
||||
//var peak = 75
|
||||
//if (insulinInterface.id == InsulinInterface.InsulinType.OREF_ULTRA_RAPID_ACTING) peak = 55 else if (insulinInterface.id == InsulinInterface.InsulinType.OREF_FREE_PEAK) peak = sp.getInt(R.string.key_insulin_oref_peak, 75)
|
||||
val csfGlucose = preppedGlucose.csfGlucoseData
|
||||
val isfGlucose = preppedGlucose.isfGlucoseData
|
||||
val basalGlucose = preppedGlucose.basalGlucoseData
|
||||
val crData = preppedGlucose.crData
|
||||
//List<DiaDatum> diaDeviations = preppedGlucose.diaDeviations;
|
||||
//List<PeakDatum> peakDeviations = preppedGlucose.peakDeviations;
|
||||
val pumpISF = pumpProfile.isf
|
||||
val pumpCarbRatio = pumpProfile.ic
|
||||
val pumpCSF = pumpISF / pumpCarbRatio
|
||||
// Autosens constraints
|
||||
val autotuneMax = sp.getDouble(R.string.key_openapsama_autosens_max, 1.2)
|
||||
val autotuneMin = sp.getDouble(R.string.key_openapsama_autosens_min, 0.7)
|
||||
val min5minCarbImpact = sp.getDouble(R.string.key_openapsama_min_5m_carbimpact, 3.0)
|
||||
|
||||
/*******Tune DIA (#57-#99) and Peak (#101-#139) disabled for the first version code below in js********************************************************************************************************
|
||||
* // tune DIA
|
||||
* var newDIA = DIA;
|
||||
* if (diaDeviations) {
|
||||
* var currentDIAMeanDev = diaDeviations[2].meanDeviation;
|
||||
* var currentDIARMSDev = diaDeviations[2].RMSDeviation;
|
||||
* //console.error(DIA,currentDIAMeanDev,currentDIARMSDev);
|
||||
* var minMeanDeviations = 1000000;
|
||||
* var minRMSDeviations = 1000000;
|
||||
* var meanBest = 2;
|
||||
* var RMSBest = 2;
|
||||
* for (var i=0; i < diaDeviations.length; i++) {
|
||||
* var meanDeviations = diaDeviations[i].meanDeviation;
|
||||
* var RMSDeviations = diaDeviations[i].RMSDeviation;
|
||||
* if (meanDeviations < minMeanDeviations) {
|
||||
* minMeanDeviations = Math.round(meanDeviations*1000)/1000;
|
||||
* meanBest = i;
|
||||
* }
|
||||
* if (RMSDeviations < minRMSDeviations) {
|
||||
* minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
|
||||
* RMSBest = i;
|
||||
* }
|
||||
* }
|
||||
* console.error("Best insulinEndTime for meanDeviations:",diaDeviations[meanBest].dia,"hours");
|
||||
* console.error("Best insulinEndTime for RMSDeviations:",diaDeviations[RMSBest].dia,"hours");
|
||||
* if ( meanBest < 2 && RMSBest < 2 ) {
|
||||
* if ( diaDeviations[1].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[1].RMSDeviation < currentDIARMSDev * 0.99 ) {
|
||||
* newDIA = diaDeviations[1].dia;
|
||||
* }
|
||||
* } else if ( meanBest > 2 && RMSBest > 2 ) {
|
||||
* if ( diaDeviations[3].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[3].RMSDeviation < currentDIARMSDev * 0.99 ) {
|
||||
* newDIA = diaDeviations[3].dia;
|
||||
* }
|
||||
* }
|
||||
* if ( newDIA > 12 ) {
|
||||
* console.error("insulinEndTime maximum is 12h: not raising further");
|
||||
* newDIA=12;
|
||||
* }
|
||||
* if ( newDIA !== DIA ) {
|
||||
* console.error("Adjusting insulinEndTime from",DIA,"to",newDIA,"hours");
|
||||
* } else {
|
||||
* console.error("Leaving insulinEndTime unchanged at",DIA,"hours");
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // tune insulinPeakTime
|
||||
* var newPeak = peak;
|
||||
* if (peakDeviations && peakDeviations[2]) {
|
||||
* var currentPeakMeanDev = peakDeviations[2].meanDeviation;
|
||||
* var currentPeakRMSDev = peakDeviations[2].RMSDeviation;
|
||||
* //console.error(currentPeakMeanDev);
|
||||
* minMeanDeviations = 1000000;
|
||||
* minRMSDeviations = 1000000;
|
||||
* meanBest = 2;
|
||||
* RMSBest = 2;
|
||||
* for (i=0; i < peakDeviations.length; i++) {
|
||||
* meanDeviations = peakDeviations[i].meanDeviation;
|
||||
* RMSDeviations = peakDeviations[i].RMSDeviation;
|
||||
* if (meanDeviations < minMeanDeviations) {
|
||||
* minMeanDeviations = Math.round(meanDeviations*1000)/1000;
|
||||
* meanBest = i;
|
||||
* }
|
||||
* if (RMSDeviations < minRMSDeviations) {
|
||||
* minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
|
||||
* RMSBest = i;
|
||||
* }
|
||||
* }
|
||||
* console.error("Best insulinPeakTime for meanDeviations:",peakDeviations[meanBest].peak,"minutes");
|
||||
* console.error("Best insulinPeakTime for RMSDeviations:",peakDeviations[RMSBest].peak,"minutes");
|
||||
* if ( meanBest < 2 && RMSBest < 2 ) {
|
||||
* if ( peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[1].RMSDeviation < currentPeakRMSDev * 0.99 ) {
|
||||
* newPeak = peakDeviations[1].peak;
|
||||
* }
|
||||
* } else if ( meanBest > 2 && RMSBest > 2 ) {
|
||||
* if ( peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[3].RMSDeviation < currentPeakRMSDev * 0.99 ) {
|
||||
* newPeak = peakDeviations[3].peak;
|
||||
* }
|
||||
* }
|
||||
* if ( newPeak !== peak ) {
|
||||
* console.error("Adjusting insulinPeakTime from",peak,"to",newPeak,"minutes");
|
||||
* } else {
|
||||
* console.error("Leaving insulinPeakTime unchanged at",peak);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
// Calculate carb ratio (CR) independently of csf and isf
|
||||
// Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
|
||||
// For now, if another meal IOB/COB stacks on top of it, consider them together
|
||||
// Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
|
||||
// Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
|
||||
|
||||
//autotune-core (lib/autotune/index.js) #149-#165
|
||||
var crTotalCarbs = 0.0
|
||||
var crTotalInsulin = 0.0
|
||||
for (i in crData!!.indices) {
|
||||
val crDatum = crData[i]
|
||||
val crBGChange = crDatum.crEndBG - crDatum.crInitialBG
|
||||
val crInsulinReq = crBGChange / isf
|
||||
//val crIOBChange = crDatum.crEndIOB - crDatum.crInitialIOB
|
||||
crDatum.crInsulinTotal = crDatum.crInitialIOB + crDatum.crInsulin + crInsulinReq
|
||||
//log(crDatum.crInitialIOB + " " + crDatum.crInsulin + " " + crInsulinReq + " " + crDatum.crInsulinTotal);
|
||||
//val cr = Round.roundTo(crDatum.crCarbs / crDatum.crInsulinTotal, 0.001)
|
||||
//log(crBGChange + " " + crInsulinReq + " " + crIOBChange + " " + crDatum.crInsulinTotal);
|
||||
//log("CRCarbs: " + crDatum.crCarbs + " CRInsulin: " + crDatum.crInsulinTotal + " CR:" + cr);
|
||||
if (crDatum.crInsulinTotal > 0) {
|
||||
crTotalCarbs += crDatum.crCarbs
|
||||
crTotalInsulin += crDatum.crInsulinTotal
|
||||
}
|
||||
}
|
||||
|
||||
//autotune-core (lib/autotune/index.js) #166-#169
|
||||
crTotalInsulin = Round.roundTo(crTotalInsulin, 0.001)
|
||||
var totalCR = 0.0
|
||||
if (crTotalInsulin != 0.0)
|
||||
totalCR = Round.roundTo(crTotalCarbs / crTotalInsulin, 0.001)
|
||||
log("crTotalCarbs: $crTotalCarbs crTotalInsulin: $crTotalInsulin totalCR: $totalCR")
|
||||
|
||||
//autotune-core (lib/autotune/index.js) #170-#209 (already hourly in aaps)
|
||||
// convert the basal profile to hourly if it isn't already
|
||||
val hourlyBasalProfile = basalProfile
|
||||
|
||||
//log(hourlyPumpProfile.toString());
|
||||
//log(hourlyBasalProfile.toString());
|
||||
val newHourlyBasalProfile = DoubleArray(24)
|
||||
for (i in 0..23) {
|
||||
newHourlyBasalProfile[i] = hourlyBasalProfile[i]
|
||||
}
|
||||
val basalUntuned = previousAutotune.basalUntuned
|
||||
|
||||
//autotune-core (lib/autotune/index.js) #210-#266
|
||||
// look at net deviations for each hour
|
||||
for (hour in 0..23) {
|
||||
var deviations = 0.0
|
||||
for (i in basalGlucose!!.indices) {
|
||||
val BGTime = Calendar.getInstance()
|
||||
//var BGTime: Date? = null
|
||||
if (basalGlucose[i].date != 0L) {
|
||||
BGTime.setTimeInMillis(basalGlucose[i].date)
|
||||
//BGTime = Date(basalGlucose[i].date)
|
||||
} else {
|
||||
log("Could not determine last BG time")
|
||||
}
|
||||
val myHour = BGTime.get(Calendar.HOUR_OF_DAY)
|
||||
//val myHour = BGTime!!.hours
|
||||
if (hour == myHour) {
|
||||
//log.debug(basalGlucose[i].deviation);
|
||||
deviations += basalGlucose[i].deviation
|
||||
}
|
||||
}
|
||||
deviations = Round.roundTo(deviations, 0.001)
|
||||
log("Hour $hour total deviations: $deviations mg/dL")
|
||||
// calculate how much less or additional basal insulin would have been required to eliminate the deviations
|
||||
// only apply 20% of the needed adjustment to keep things relatively stable
|
||||
var basalNeeded = 0.2 * deviations / isf
|
||||
basalNeeded = Round.roundTo(basalNeeded, 0.01)
|
||||
// if basalNeeded is positive, adjust each of the 1-3 hour prior basals by 10% of the needed adjustment
|
||||
log("Hour $hour basal adjustment needed: $basalNeeded U/hr")
|
||||
if (basalNeeded > 0) {
|
||||
for (offset in -3..-1) {
|
||||
var offsetHour = hour + offset
|
||||
if (offsetHour < 0) {
|
||||
offsetHour += 24
|
||||
}
|
||||
//log.debug(offsetHour);
|
||||
newHourlyBasalProfile[offsetHour] = newHourlyBasalProfile[offsetHour] + basalNeeded / 3
|
||||
newHourlyBasalProfile[offsetHour] = Round.roundTo(newHourlyBasalProfile[offsetHour], 0.001)
|
||||
}
|
||||
// otherwise, figure out the percentage reduction required to the 1-3 hour prior basals
|
||||
// and adjust all of them downward proportionally
|
||||
} else if (basalNeeded < 0) {
|
||||
var threeHourBasal = 0.0
|
||||
for (offset in -3..-1) {
|
||||
var offsetHour = hour + offset
|
||||
if (offsetHour < 0) {
|
||||
offsetHour += 24
|
||||
}
|
||||
threeHourBasal += newHourlyBasalProfile[offsetHour]
|
||||
}
|
||||
val adjustmentRatio = 1.0 + basalNeeded / threeHourBasal
|
||||
//log.debug(adjustmentRatio);
|
||||
for (offset in -3..-1) {
|
||||
var offsetHour = hour + offset
|
||||
if (offsetHour < 0) {
|
||||
offsetHour += 24
|
||||
}
|
||||
newHourlyBasalProfile[offsetHour] = newHourlyBasalProfile[offsetHour] * adjustmentRatio
|
||||
newHourlyBasalProfile[offsetHour] = Round.roundTo(newHourlyBasalProfile[offsetHour], 0.001)
|
||||
}
|
||||
}
|
||||
}
|
||||
//autotune-core (lib/autotune/index.js) #267-#294
|
||||
for (hour in 0..23) {
|
||||
//log.debug(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2);
|
||||
// cap adjustments at autosens_max and autosens_min
|
||||
val maxRate = pumpBasalProfile[hour] * autotuneMax
|
||||
val minRate = pumpBasalProfile[hour] * autotuneMin
|
||||
if (newHourlyBasalProfile[hour] > maxRate) {
|
||||
log("Limiting hour " + hour + " basal to " + Round.roundTo(maxRate, 0.01) + " (which is " + Round.roundTo(autotuneMax, 0.01) + " * pump basal of " + pumpBasalProfile[hour] + ")")
|
||||
//log.debug("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is 20% above pump basal of",hourlyPumpProfile[hour].rate,")");
|
||||
newHourlyBasalProfile[hour] = maxRate
|
||||
} else if (newHourlyBasalProfile[hour] < minRate) {
|
||||
log("Limiting hour " + hour + " basal to " + Round.roundTo(minRate, 0.01) + " (which is " + autotuneMin + " * pump basal of " + newHourlyBasalProfile[hour] + ")")
|
||||
//log.debug("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is 20% below pump basal of",hourlyPumpProfile[hour].rate,")");
|
||||
newHourlyBasalProfile[hour] = minRate
|
||||
}
|
||||
newHourlyBasalProfile[hour] = Round.roundTo(newHourlyBasalProfile[hour], 0.001)
|
||||
}
|
||||
|
||||
// some hours of the day rarely have data to tune basals due to meals.
|
||||
// when no adjustments are needed to a particular hour, we should adjust it toward the average of the
|
||||
// periods before and after it that do have data to be tuned
|
||||
var lastAdjustedHour = 0
|
||||
// scan through newHourlyBasalProfile and find hours where the rate is unchanged
|
||||
//autotune-core (lib/autotune/index.js) #302-#323
|
||||
for (hour in 0..23) {
|
||||
if (hourlyBasalProfile[hour] == newHourlyBasalProfile[hour]) {
|
||||
var nextAdjustedHour = 23
|
||||
for (nextHour in hour..23) {
|
||||
if (hourlyBasalProfile[nextHour] != newHourlyBasalProfile[nextHour]) {
|
||||
nextAdjustedHour = nextHour
|
||||
break
|
||||
//} else {
|
||||
// log("At hour: "+nextHour +" " + hourlyBasalProfile[nextHour] + " " +newHourlyBasalProfile[nextHour]);
|
||||
}
|
||||
}
|
||||
//log.debug(hour, newHourlyBasalProfile);
|
||||
newHourlyBasalProfile[hour] = Round.roundTo(0.8 * hourlyBasalProfile[hour] + 0.1 * newHourlyBasalProfile[lastAdjustedHour] + 0.1 * newHourlyBasalProfile[nextAdjustedHour], 0.001)
|
||||
basalUntuned[hour]++
|
||||
log("Adjusting hour " + hour + " basal from " + hourlyBasalProfile[hour] + " to " + newHourlyBasalProfile[hour] + " based on hour " + lastAdjustedHour + " = " + newHourlyBasalProfile[lastAdjustedHour] + " and hour " + nextAdjustedHour + " = " + newHourlyBasalProfile[nextAdjustedHour])
|
||||
} else {
|
||||
lastAdjustedHour = hour
|
||||
}
|
||||
}
|
||||
//log(newHourlyBasalProfile.toString());
|
||||
basalProfile = newHourlyBasalProfile
|
||||
|
||||
// Calculate carb ratio (CR) independently of csf and isf
|
||||
// Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
|
||||
// For now, if another meal IOB/COB stacks on top of it, consider them together
|
||||
// Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
|
||||
// Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
|
||||
|
||||
// calculate net deviations while carbs are absorbing
|
||||
// measured from carb entry until COB and deviations both drop to zero
|
||||
var deviations = 0.0
|
||||
var mealCarbs = 0
|
||||
var totalMealCarbs = 0
|
||||
var totalDeviations = 0.0
|
||||
val fullNewCSF: Double
|
||||
//log.debug(CSFGlucose[0].mealAbsorption);
|
||||
//log.debug(CSFGlucose[0]);
|
||||
//autotune-core (lib/autotune/index.js) #346-#365
|
||||
for (i in csfGlucose!!.indices) {
|
||||
//log.debug(CSFGlucose[i].mealAbsorption, i);
|
||||
if (csfGlucose[i].mealAbsorption === "start") {
|
||||
deviations = 0.0
|
||||
mealCarbs = csfGlucose[i].mealCarbs
|
||||
} else if (csfGlucose[i].mealAbsorption === "end") {
|
||||
deviations += csfGlucose[i].deviation
|
||||
// compare the sum of deviations from start to end vs. current csf * mealCarbs
|
||||
//log.debug(csf,mealCarbs);
|
||||
//val csfRise = csf * mealCarbs
|
||||
//log.debug(deviations,isf);
|
||||
//log.debug("csfRise:",csfRise,"deviations:",deviations);
|
||||
totalMealCarbs += mealCarbs
|
||||
totalDeviations += deviations
|
||||
} else {
|
||||
//todo Philoul check 0 * min5minCarbImpact ???
|
||||
deviations += Math.max(0 * min5minCarbImpact, csfGlucose[i].deviation)
|
||||
mealCarbs = Math.max(mealCarbs, csfGlucose[i].mealCarbs)
|
||||
}
|
||||
}
|
||||
// at midnight, write down the mealcarbs as total meal carbs (to prevent special case of when only one meal and it not finishing absorbing by midnight)
|
||||
// TODO: figure out what to do with dinner carbs that don't finish absorbing by midnight
|
||||
if (totalMealCarbs == 0) {
|
||||
totalMealCarbs += mealCarbs
|
||||
}
|
||||
if (totalDeviations == 0.0) {
|
||||
totalDeviations += deviations
|
||||
}
|
||||
//log.debug(totalDeviations, totalMealCarbs);
|
||||
fullNewCSF = if (totalMealCarbs == 0) {
|
||||
// if no meals today, csf is unchanged
|
||||
csf
|
||||
} else {
|
||||
// how much change would be required to account for all of the deviations
|
||||
Round.roundTo(totalDeviations / totalMealCarbs, 0.01)
|
||||
}
|
||||
// only adjust by 20%
|
||||
var newCSF = 0.8 * csf + 0.2 * fullNewCSF
|
||||
// safety cap csf
|
||||
if (pumpCSF != 0.0) {
|
||||
val maxCSF = pumpCSF * autotuneMax
|
||||
val minCSF = pumpCSF * autotuneMin
|
||||
if (newCSF > maxCSF) {
|
||||
log("Limiting csf to " + Round.roundTo(maxCSF, 0.01) + " (which is " + autotuneMax + "* pump csf of " + pumpCSF + ")")
|
||||
newCSF = maxCSF
|
||||
} else if (newCSF < minCSF) {
|
||||
log("Limiting csf to " + Round.roundTo(minCSF, 0.01) + " (which is" + autotuneMin + "* pump csf of " + pumpCSF + ")")
|
||||
newCSF = minCSF
|
||||
} //else { log.debug("newCSF",newCSF,"is close enough to",pumpCSF); }
|
||||
}
|
||||
val oldCSF = Round.roundTo(csf, 0.001)
|
||||
newCSF = Round.roundTo(newCSF, 0.001)
|
||||
totalDeviations = Round.roundTo(totalDeviations, 0.001)
|
||||
log("totalMealCarbs: $totalMealCarbs totalDeviations: $totalDeviations oldCSF $oldCSF fullNewCSF: $fullNewCSF newCSF: $newCSF")
|
||||
// this is where csf is set based on the outputs
|
||||
//if (newCSF != 0.0) {
|
||||
// csf = newCSF
|
||||
//}
|
||||
var fullNewCR: Double
|
||||
fullNewCR = if (totalCR == 0.0) {
|
||||
// if no meals today, CR is unchanged
|
||||
carbRatio
|
||||
} else {
|
||||
// how much change would be required to account for all of the deviations
|
||||
totalCR
|
||||
}
|
||||
// don't tune CR out of bounds
|
||||
var maxCR = pumpCarbRatio * autotuneMax
|
||||
if (maxCR > 150) {
|
||||
maxCR = 150.0
|
||||
}
|
||||
var minCR = pumpCarbRatio * autotuneMin
|
||||
if (minCR < 3) {
|
||||
minCR = 3.0
|
||||
}
|
||||
// safety cap fullNewCR
|
||||
if (pumpCarbRatio != 0.0) {
|
||||
if (fullNewCR > maxCR) {
|
||||
log("Limiting fullNewCR from " + fullNewCR + " to " + Round.roundTo(maxCR, 0.01) + " (which is " + autotuneMax + " * pump CR of " + pumpCarbRatio + ")")
|
||||
fullNewCR = maxCR
|
||||
} else if (fullNewCR < minCR) {
|
||||
log("Limiting fullNewCR from " + fullNewCR + " to " + Round.roundTo(minCR, 0.01) + " (which is " + autotuneMin + " * pump CR of " + pumpCarbRatio + ")")
|
||||
fullNewCR = minCR
|
||||
} //else { log.debug("newCR",newCR,"is close enough to",pumpCarbRatio); }
|
||||
}
|
||||
// only adjust by 20%
|
||||
var newCR = 0.8 * carbRatio + 0.2 * fullNewCR
|
||||
// safety cap newCR
|
||||
if (pumpCarbRatio != 0.0) {
|
||||
if (newCR > maxCR) {
|
||||
log("Limiting CR to " + Round.roundTo(maxCR, 0.01) + " (which is " + autotuneMax + " * pump CR of " + pumpCarbRatio + ")")
|
||||
newCR = maxCR
|
||||
} else if (newCR < minCR) {
|
||||
log("Limiting CR to " + Round.roundTo(minCR, 0.01) + " (which is " + autotuneMin + " * pump CR of " + pumpCarbRatio + ")")
|
||||
newCR = minCR
|
||||
} //else { log.debug("newCR",newCR,"is close enough to",pumpCarbRatio); }
|
||||
}
|
||||
newCR = Round.roundTo(newCR, 0.001)
|
||||
log("oldCR: $carbRatio fullNewCR: $fullNewCR newCR: $newCR")
|
||||
// this is where CR is set based on the outputs
|
||||
//var ISFFromCRAndCSF = isf;
|
||||
if (newCR != 0.0) {
|
||||
carbRatio = newCR
|
||||
//ISFFromCRAndCSF = Math.round( carbRatio * csf * 1000)/1000;
|
||||
}
|
||||
|
||||
// calculate median deviation and bgi in data attributable to isf
|
||||
val isfDeviations: MutableList<Double> = ArrayList()
|
||||
val bGIs: MutableList<Double> = ArrayList()
|
||||
val avgDeltas: MutableList<Double> = ArrayList()
|
||||
val ratios: MutableList<Double> = ArrayList()
|
||||
var count = 0
|
||||
for (i in isfGlucose!!.indices) {
|
||||
val deviation = isfGlucose[i].deviation
|
||||
isfDeviations.add(deviation)
|
||||
val BGI = isfGlucose[i].bgi
|
||||
bGIs.add(BGI)
|
||||
val avgDelta = isfGlucose[i].avgDelta
|
||||
avgDeltas.add(avgDelta)
|
||||
val ratio = 1 + deviation / BGI
|
||||
//log.debug("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio);
|
||||
ratios.add(ratio)
|
||||
count++
|
||||
}
|
||||
Collections.sort(avgDeltas)
|
||||
Collections.sort(bGIs)
|
||||
Collections.sort(isfDeviations)
|
||||
Collections.sort(ratios)
|
||||
var p50deviation = IobCobCalculatorPlugin.percentile(isfDeviations.toTypedArray(), 0.50)
|
||||
var p50BGI = IobCobCalculatorPlugin.percentile(bGIs.toTypedArray(), 0.50)
|
||||
val p50ratios = Round.roundTo(IobCobCalculatorPlugin.percentile(ratios.toTypedArray(), 0.50), 0.001)
|
||||
var fullNewISF = isf
|
||||
if (count < 10) {
|
||||
// leave isf unchanged if fewer than 5 isf data points
|
||||
log("Only found " + isfGlucose.size + " ISF data points, leaving ISF unchanged at " + isf)
|
||||
} else {
|
||||
// calculate what adjustments to isf would have been necessary to bring median deviation to zero
|
||||
fullNewISF = isf * p50ratios
|
||||
}
|
||||
fullNewISF = Round.roundTo(fullNewISF, 0.001)
|
||||
// adjust the target isf to be a weighted average of fullNewISF and pumpISF
|
||||
val adjustmentFraction: Double
|
||||
/*
|
||||
// TODO: philoul may be allow adjustmentFraction in settings with safety limits ?)
|
||||
if (typeof(pumpProfile.autotune_isf_adjustmentFraction) !== 'undefined') {
|
||||
adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction;
|
||||
} else {*/
|
||||
adjustmentFraction = 1.0
|
||||
// }
|
||||
|
||||
// low autosens ratio = high isf
|
||||
val maxISF = pumpISF / autotuneMin
|
||||
// high autosens ratio = low isf
|
||||
val minISF = pumpISF / autotuneMax
|
||||
var adjustedISF = 0.0
|
||||
var newISF = 0.0
|
||||
if (pumpISF != 0.0) {
|
||||
adjustedISF = if (fullNewISF < 0) {
|
||||
isf
|
||||
} else {
|
||||
adjustmentFraction * fullNewISF + (1 - adjustmentFraction) * pumpISF
|
||||
}
|
||||
// cap adjustedISF before applying 10%
|
||||
//log.debug(adjustedISF, maxISF, minISF);
|
||||
if (adjustedISF > maxISF) {
|
||||
log("Limiting adjusted isf of " + Round.roundTo(adjustedISF, 0.01) + " to " + Round.roundTo(maxISF, 0.01) + "(which is pump isf of " + pumpISF + "/" + autotuneMin + ")")
|
||||
adjustedISF = maxISF
|
||||
} else if (adjustedISF < minISF) {
|
||||
log("Limiting adjusted isf of" + Round.roundTo(adjustedISF, 0.01) + " to " + Round.roundTo(minISF, 0.01) + "(which is pump isf of " + pumpISF + "/" + autotuneMax + ")")
|
||||
adjustedISF = minISF
|
||||
}
|
||||
|
||||
// and apply 20% of that adjustment
|
||||
newISF = 0.8 * isf + 0.2 * adjustedISF
|
||||
if (newISF > maxISF) {
|
||||
log("Limiting isf of" + Round.roundTo(newISF, 0.01) + "to" + Round.roundTo(maxISF, 0.01) + "(which is pump isf of" + pumpISF + "/" + autotuneMin + ")")
|
||||
newISF = maxISF
|
||||
} else if (newISF < minISF) {
|
||||
log("Limiting isf of" + Round.roundTo(newISF, 0.01) + "to" + Round.roundTo(minISF, 0.01) + "(which is pump isf of" + pumpISF + "/" + autotuneMax + ")")
|
||||
newISF = minISF
|
||||
}
|
||||
}
|
||||
newISF = Round.roundTo(newISF, 0.001)
|
||||
//log.debug(avgRatio);
|
||||
//log.debug(newISF);
|
||||
p50deviation = Round.roundTo(p50deviation, 0.001)
|
||||
p50BGI = Round.roundTo(p50BGI, 0.001)
|
||||
adjustedISF = Round.roundTo(adjustedISF, 0.001)
|
||||
log("p50deviation: $p50deviation p50BGI $p50BGI p50ratios: $p50ratios Old isf: $isf fullNewISF: $fullNewISF adjustedISF: $adjustedISF newISF: $newISF")
|
||||
if (newISF != 0.0) {
|
||||
isf = newISF
|
||||
}
|
||||
previousAutotune.from = preppedGlucose.from
|
||||
previousAutotune.basal = basalProfile
|
||||
previousAutotune.isf = isf
|
||||
previousAutotune.ic = Round.roundTo(carbRatio, 0.001)
|
||||
previousAutotune.basalUntuned = basalUntuned
|
||||
/* code prepared for future dia/peak integration
|
||||
previousAutotune.dia=newDia;
|
||||
previousAutotune.peak = newPeak ;
|
||||
if (diaDeviations || peakDeviations) {
|
||||
autotuneOutput.useCustomPeakTime = true;
|
||||
}
|
||||
*/
|
||||
previousAutotune.updateProfile()
|
||||
return previousAutotune
|
||||
}
|
||||
|
||||
private fun log(message: String) {
|
||||
autotuneFS.atLog("[Core] $message")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune
|
||||
|
||||
import info.nightscout.androidaps.interfaces.ResourceHelper
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.LoggerUtils
|
||||
import info.nightscout.androidaps.R
|
||||
import org.json.JSONException
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AutotuneFS @Inject constructor(
|
||||
private val rh: ResourceHelper,
|
||||
private val loggerUtils: LoggerUtils
|
||||
) {
|
||||
|
||||
val AUTOTUNEFOLDER = "autotune"
|
||||
val SETTINGSFOLDER = "settings"
|
||||
val RECOMMENDATIONS = "autotune_recommendations.log"
|
||||
val ENTRIESPREF = "aaps-entries."
|
||||
val TREATMENTSPREF = "aaps-treatments."
|
||||
val AAPSBOLUSESPREF = "aaps-boluses."
|
||||
val PREPPEDPREF = "aaps-autotune."
|
||||
val SETTINGS = "settings.json"
|
||||
val PROFIL = "profil"
|
||||
val PUMPPROFILE = "pumpprofile.json"
|
||||
val TUNEDPROFILE = "newaapsprofile."
|
||||
val LOGPREF = "autotune."
|
||||
val ZIPPREF = "autotune_"
|
||||
lateinit var autotunePath: File
|
||||
lateinit var autotuneSettings: File
|
||||
private var logString = ""
|
||||
val BUFFER_SIZE = 2048
|
||||
private val log = LoggerFactory.getLogger(AutotunePlugin::class.java)
|
||||
|
||||
/*****************************************************************************
|
||||
* Create autotune folder for all files created during an autotune session
|
||||
*****************************************************************************/
|
||||
fun createAutotuneFolder() {
|
||||
//create autotune subfolder for autotune files if not exists
|
||||
autotunePath = File(loggerUtils.logDirectory, AUTOTUNEFOLDER)
|
||||
if (!(autotunePath.exists() && autotunePath.isDirectory)) {
|
||||
autotunePath.mkdir()
|
||||
log("Create $AUTOTUNEFOLDER subfolder in ${loggerUtils.logDirectory}")
|
||||
}
|
||||
autotuneSettings = File(loggerUtils.logDirectory, SETTINGSFOLDER)
|
||||
if (!(autotuneSettings.exists() && autotuneSettings.isDirectory)) {
|
||||
autotuneSettings.mkdir()
|
||||
log("Create $SETTINGSFOLDER subfolder in ${loggerUtils.logDirectory}")
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* between each run of autotune, clean autotune folder content
|
||||
*****************************************************************************/
|
||||
fun deleteAutotuneFiles() {
|
||||
autotunePath.listFiles()?.let { listFiles ->
|
||||
for (file in listFiles) {
|
||||
if (file.isFile) file.delete()
|
||||
}
|
||||
}
|
||||
autotuneSettings.listFiles()?.let { listFiles ->
|
||||
for (file in listFiles) {
|
||||
if (file.isFile) file.delete()
|
||||
}
|
||||
}
|
||||
log("Delete previous Autotune files")
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* Create a JSON autotune files or settings files
|
||||
*****************************************************************************/
|
||||
fun exportSettings(settings: String) {
|
||||
createAutotunefile(SETTINGS, settings, true)
|
||||
}
|
||||
|
||||
fun exportPumpProfile(profile: ATProfile) {
|
||||
createAutotunefile(PUMPPROFILE, profile.profiletoOrefJSON(), true)
|
||||
createAutotunefile(PUMPPROFILE, profile.profiletoOrefJSON())
|
||||
}
|
||||
|
||||
fun exportTunedProfile(tunedProfile: ATProfile) {
|
||||
createAutotunefile(TUNEDPROFILE + formatDate(tunedProfile.from) + ".json", tunedProfile.profiletoOrefJSON())
|
||||
try {
|
||||
createAutotunefile(rh.gs(R.string.autotune_tunedprofile_name) + ".json", tunedProfile.profiletoOrefJSON(), true)
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun exportEntries(autotuneIob: AutotuneIob) {
|
||||
try {
|
||||
createAutotunefile(ENTRIESPREF + formatDate(autotuneIob.startBG) + ".json", autotuneIob.glucoseToJSON())
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun exportTreatments(autotuneIob: AutotuneIob) {
|
||||
try {
|
||||
createAutotunefile(TREATMENTSPREF + formatDate(autotuneIob.startBG) + ".json", autotuneIob.nsHistoryToJSON())
|
||||
createAutotunefile(AAPSBOLUSESPREF + formatDate(autotuneIob.startBG) + ".json", autotuneIob.bolusesToJSON())
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun exportPreppedGlucose(preppedGlucose: PreppedGlucose) {
|
||||
createAutotunefile(PREPPEDPREF + formatDate(preppedGlucose.from) + ".json", preppedGlucose.toString(2))
|
||||
}
|
||||
|
||||
fun exportResult(result: String) {
|
||||
createAutotunefile(RECOMMENDATIONS, result)
|
||||
}
|
||||
|
||||
fun exportLog(lastRun: Long, index: Int = 0) {
|
||||
val suffix = if (index == 0) "" else "_" + index
|
||||
log("Create " + LOGPREF + formatDate(lastRun) + suffix + ".log" + " file in " + AUTOTUNEFOLDER + " folder")
|
||||
createAutotunefile(LOGPREF + formatDate(lastRun) + suffix + ".log", logString)
|
||||
logString = ""
|
||||
}
|
||||
|
||||
fun exportLogAndZip(lastRun: Long) {
|
||||
log("Create " + LOGPREF + formatDate(lastRun) + ".log" + " file in " + AUTOTUNEFOLDER + " folder")
|
||||
createAutotunefile(LOGPREF + formatDate(lastRun) + ".log", logString)
|
||||
zipAutotune(lastRun)
|
||||
logString = ""
|
||||
}
|
||||
|
||||
private fun createAutotunefile(fileName: String, stringFile: String, isSettingFile: Boolean = false) {
|
||||
val autotuneFile = File(if (isSettingFile) autotuneSettings.absolutePath else autotunePath.absolutePath, fileName)
|
||||
try {
|
||||
val fw = FileWriter(autotuneFile)
|
||||
val pw = PrintWriter(fw)
|
||||
pw.println(stringFile)
|
||||
pw.close()
|
||||
fw.close()
|
||||
log("Create " + fileName + " file in " + (if (isSettingFile) SETTINGSFOLDER else AUTOTUNEFOLDER) + " folder")
|
||||
} catch (e: FileNotFoundException) {
|
||||
//log.error("Unhandled exception", e);
|
||||
} catch (e: IOException) {
|
||||
//log.error("Unhandled exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************************************************
|
||||
* create a zip file with all autotune files and settings in autotune folder at the end of run
|
||||
**********************************************************************************/
|
||||
fun zipAutotune(lastRun: Long) {
|
||||
try {
|
||||
val zipFileName = ZIPPREF + formatDate(lastRun, true) + ".zip"
|
||||
val zipFile = File(loggerUtils.logDirectory, zipFileName)
|
||||
val out = ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile)))
|
||||
zipDirectory(autotunePath, autotunePath.name, out)
|
||||
zipDirectory(autotuneSettings, autotuneSettings.name, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
log("Create $zipFileName file in ${loggerUtils.logDirectory} folder")
|
||||
} catch (e: IOException) {
|
||||
//log.error("Unhandled exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private fun log(message: String) {
|
||||
atLog("[FS] $message")
|
||||
}
|
||||
|
||||
fun atLog(message: String) {
|
||||
logString += "$message\n"
|
||||
log.debug(message)
|
||||
}
|
||||
|
||||
private fun zipDirectory(folder: File, parentFolder: String, out: ZipOutputStream) {
|
||||
folder.listFiles()?.let { listFiles ->
|
||||
for (file in listFiles) {
|
||||
if (file.isDirectory) {
|
||||
zipDirectory(file, parentFolder + "/" + file.name, out)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
out.putNextEntry(ZipEntry(parentFolder + "/" + file.name))
|
||||
val bis = BufferedInputStream(FileInputStream(file))
|
||||
//long bytesRead = 0;
|
||||
val bytesIn = ByteArray(BUFFER_SIZE)
|
||||
var read: Int
|
||||
while (bis.read(bytesIn).also { read = it } != -1) {
|
||||
out.write(bytesIn, 0, read)
|
||||
}
|
||||
out.closeEntry()
|
||||
} catch (e: IOException) {
|
||||
//log.error("Unhandled exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(date: Long, dateHour: Boolean = false): String {
|
||||
val dateFormat = if (dateHour) SimpleDateFormat("yyyy-MM-dd_HH-mm-ss") else SimpleDateFormat("yyyy-MM-dd")
|
||||
return dateFormat.format(date)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import dagger.android.HasAndroidInjector
|
||||
import dagger.android.support.DaggerFragment
|
||||
import info.nightscout.androidaps.Constants
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.databinding.AutotuneFragmentBinding
|
||||
import info.nightscout.androidaps.dialogs.ProfileViewerDialog
|
||||
import info.nightscout.androidaps.plugins.bus.RxBus
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
|
||||
import info.nightscout.androidaps.plugins.general.autotune.events.EventAutotuneUpdateGui
|
||||
import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin
|
||||
import info.nightscout.androidaps.plugins.profile.local.events.EventLocalProfileChanged
|
||||
import info.nightscout.androidaps.data.LocalInsulin
|
||||
import info.nightscout.androidaps.data.ProfileSealed
|
||||
import info.nightscout.androidaps.database.entities.UserEntry
|
||||
import info.nightscout.androidaps.database.entities.ValueWithUnit
|
||||
import info.nightscout.androidaps.extensions.runOnUiThread
|
||||
import info.nightscout.androidaps.interfaces.*
|
||||
import info.nightscout.androidaps.logging.UserEntryLogger
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.FabricPrivacy
|
||||
import info.nightscout.androidaps.utils.MidnightTime
|
||||
import info.nightscout.androidaps.utils.Round
|
||||
import info.nightscout.androidaps.utils.alertDialogs.OKDialog.showConfirmation
|
||||
import info.nightscout.shared.SafeParse
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.text.DecimalFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class AutotuneFragment : DaggerFragment() {
|
||||
@Inject lateinit var profileFunction: ProfileFunction
|
||||
@Inject lateinit var autotunePlugin: AutotunePlugin
|
||||
@Inject lateinit var autotuneFS: AutotuneFS
|
||||
@Inject lateinit var sp: SP
|
||||
@Inject lateinit var dateUtil: DateUtil
|
||||
@Inject lateinit var activePlugin: ActivePlugin
|
||||
@Inject lateinit var localProfilePlugin: LocalProfilePlugin
|
||||
@Inject lateinit var fabricPrivacy: FabricPrivacy
|
||||
@Inject lateinit var uel: UserEntryLogger
|
||||
@Inject lateinit var rh: ResourceHelper
|
||||
@Inject lateinit var rxBus: RxBus
|
||||
@Inject lateinit var injector: HasAndroidInjector
|
||||
|
||||
private var disposable: CompositeDisposable = CompositeDisposable()
|
||||
private val log = LoggerFactory.getLogger(AutotunePlugin::class.java)
|
||||
private var _binding: AutotuneFragmentBinding? = null
|
||||
private lateinit var profileStore: ProfileStore
|
||||
private var profileName = ""
|
||||
private lateinit var profile: ATProfile
|
||||
// This property is only valid between onCreateView and
|
||||
// onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = AutotuneFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
autotunePlugin.lastRun = sp.getLong(R.string.key_autotune_last_run, 0)
|
||||
if (autotunePlugin.lastNbDays.isNullOrEmpty())
|
||||
autotunePlugin.lastNbDays = sp.getInt(R.string.key_autotune_default_tune_days, 5).toString()
|
||||
val defaultValue = sp.getInt(R.string.key_autotune_default_tune_days, 5).toDouble()
|
||||
profileStore = activePlugin.activeProfileSource.profile ?: ProfileStore(injector, JSONObject(), dateUtil)
|
||||
profileName = if (binding.profileList.text.toString() == rh.gs(R.string.active)) "" else binding.profileList.text.toString()
|
||||
profileFunction.getProfile()?.let { currentProfile ->
|
||||
profile = ATProfile(profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(it) } ?:currentProfile, LocalInsulin(""), injector)
|
||||
}
|
||||
|
||||
binding.tuneDays.setParams(
|
||||
savedInstanceState?.getDouble("tunedays")
|
||||
?: defaultValue, 1.0, 30.0, 1.0, DecimalFormat("0"), false, null, textWatcher)
|
||||
binding.autotuneRun.setOnClickListener {
|
||||
val daysBack = SafeParse.stringToInt(binding.tuneDays.text)
|
||||
autotunePlugin.calculationRunning = true
|
||||
autotunePlugin.lastNbDays = daysBack.toString()
|
||||
log("Run Autotune $profileName, $daysBack days")
|
||||
Thread(Runnable {
|
||||
autotunePlugin.aapsAutotune(daysBack, false, profileName)
|
||||
}).start()
|
||||
updateGui()
|
||||
}
|
||||
binding.profileList.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, _ ->
|
||||
if (!autotunePlugin.calculationRunning)
|
||||
{
|
||||
profileName = if (binding.profileList.text.toString() == rh.gs(R.string.active)) "" else binding.profileList.text.toString()
|
||||
profileFunction.getProfile()?.let { currentProfile ->
|
||||
profile = ATProfile(profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(it) } ?:currentProfile, LocalInsulin(""), injector)
|
||||
}
|
||||
autotunePlugin.selectedProfile = profileName
|
||||
resetParam()
|
||||
}
|
||||
updateGui()
|
||||
}
|
||||
|
||||
binding.autotuneCopylocal.setOnClickListener {
|
||||
val localName = rh.gs(R.string.autotune_tunedprofile_name) + " " + dateUtil.dateAndTimeString(autotunePlugin.lastRun)
|
||||
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
|
||||
autotunePlugin.tunedProfile?.let { tunedProfile ->
|
||||
showConfirmation(requireContext(),
|
||||
rh.gs(R.string.autotune_copy_localprofile_button),
|
||||
rh.gs(R.string.autotune_copy_local_profile_message) + "\n" + localName + " " + dateUtil.dateAndTimeString(autotunePlugin.lastRun),
|
||||
Runnable {
|
||||
localProfilePlugin.addProfile(localProfilePlugin.copyFrom(tunedProfile.getProfile(circadian), localName))
|
||||
rxBus.send(EventLocalProfileChanged())
|
||||
uel.log(
|
||||
UserEntry.Action.NEW_PROFILE,
|
||||
UserEntry.Sources.Autotune,
|
||||
ValueWithUnit.SimpleString(localName)
|
||||
)
|
||||
updateGui()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
binding.autotuneUpdateProfile.setOnClickListener {
|
||||
val localName = autotunePlugin.pumpProfile.profilename
|
||||
showConfirmation(requireContext(),
|
||||
rh.gs(R.string.autotune_update_input_profile_button),
|
||||
rh.gs(R.string.autotune_update_local_profile_message, localName),
|
||||
Runnable {
|
||||
autotunePlugin.tunedProfile?.profilename = localName
|
||||
autotunePlugin.updateProfile(autotunePlugin.tunedProfile)
|
||||
autotunePlugin.updateButtonVisibility = View.GONE
|
||||
uel.log(
|
||||
UserEntry.Action.STORE_PROFILE,
|
||||
UserEntry.Sources.Autotune,
|
||||
ValueWithUnit.SimpleString(localName)
|
||||
)
|
||||
updateGui()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.autotuneRevertProfile.setOnClickListener {
|
||||
val localName = autotunePlugin.pumpProfile.profilename
|
||||
showConfirmation(requireContext(),
|
||||
rh.gs(R.string.autotune_revert_input_profile_button),
|
||||
rh.gs(R.string.autotune_revert_local_profile_message, localName),
|
||||
Runnable {
|
||||
autotunePlugin.tunedProfile?.profilename = ""
|
||||
autotunePlugin.updateProfile(autotunePlugin.pumpProfile)
|
||||
autotunePlugin.updateButtonVisibility = View.VISIBLE
|
||||
uel.log(
|
||||
UserEntry.Action.STORE_PROFILE,
|
||||
UserEntry.Sources.Autotune,
|
||||
ValueWithUnit.SimpleString(localName)
|
||||
)
|
||||
updateGui()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.autotuneCheckInputProfile.setOnClickListener {
|
||||
val pumpProfile = profileFunction.getProfile()?.let { currentProfile ->
|
||||
profileStore.getSpecificProfile(profileName)?.let { specificProfile ->
|
||||
ATProfile(ProfileSealed.Pure(specificProfile), LocalInsulin(""), injector).also {
|
||||
it.profilename = profileName
|
||||
}
|
||||
}
|
||||
?: ATProfile(currentProfile, LocalInsulin(""), injector).also {
|
||||
it.profilename = profileFunction.getProfileName()
|
||||
}
|
||||
}
|
||||
pumpProfile?.let {
|
||||
ProfileViewerDialog().also { pvd ->
|
||||
pvd.arguments = Bundle().also {
|
||||
it.putLong("time", dateUtil.now())
|
||||
it.putInt("mode", ProfileViewerDialog.Mode.CUSTOM_PROFILE.ordinal)
|
||||
it.putString("customProfile", pumpProfile.profile.toPureNsJson(dateUtil).toString())
|
||||
it.putString("customProfileUnits", profileFunction.getUnits().asText)
|
||||
it.putString("customProfileName", pumpProfile.profilename)
|
||||
}
|
||||
}.show(childFragmentManager, "ProfileViewDialog")
|
||||
}
|
||||
}
|
||||
|
||||
binding.autotuneCompare.setOnClickListener {
|
||||
val pumpProfile = autotunePlugin.pumpProfile
|
||||
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
|
||||
val tunedprofile = if (circadian) autotunePlugin.tunedProfile?.circadianProfile else autotunePlugin.tunedProfile?.profile
|
||||
ProfileViewerDialog().also { pvd ->
|
||||
pvd.arguments = Bundle().also {
|
||||
it.putLong("time", dateUtil.now())
|
||||
it.putInt("mode", ProfileViewerDialog.Mode.PROFILE_COMPARE.ordinal)
|
||||
it.putString("customProfile", pumpProfile.profile.toPureNsJson(dateUtil).toString())
|
||||
it.putString("customProfile2", tunedprofile?.toPureNsJson(dateUtil).toString())
|
||||
it.putString("customProfileUnits", profileFunction.getUnits().asText)
|
||||
it.putString("customProfileName", pumpProfile.profilename + "\n" + rh.gs(R.string.autotune_tunedprofile_name))
|
||||
}
|
||||
}.show(childFragmentManager, "ProfileViewDialog")
|
||||
}
|
||||
|
||||
binding.autotuneProfileswitch.setOnClickListener {
|
||||
val tunedProfile = autotunePlugin.tunedProfile
|
||||
autotunePlugin.updateProfile(tunedProfile)
|
||||
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
|
||||
tunedProfile?.let { tunedP ->
|
||||
tunedP.profileStore(circadian)?.let {
|
||||
showConfirmation(requireContext(),
|
||||
rh.gs(R.string.activate_profile) + ": " + tunedP.profilename + " ?",
|
||||
Runnable {
|
||||
uel.log(
|
||||
UserEntry.Action.STORE_PROFILE,
|
||||
UserEntry.Sources.Autotune,
|
||||
ValueWithUnit.SimpleString(tunedP.profilename)
|
||||
)
|
||||
val now = dateUtil.now()
|
||||
if (profileFunction.createProfileSwitch(
|
||||
it,
|
||||
profileName = tunedP.profilename,
|
||||
durationInMinutes = 0,
|
||||
percentage = 100,
|
||||
timeShiftInHours = 0,
|
||||
timestamp = now
|
||||
)
|
||||
) {
|
||||
uel.log(
|
||||
UserEntry.Action.PROFILE_SWITCH,
|
||||
UserEntry.Sources.Autotune,
|
||||
"Autotune AutoSwitch",
|
||||
ValueWithUnit.SimpleString(autotunePlugin.tunedProfile!!.profilename)
|
||||
)
|
||||
}
|
||||
rxBus.send(EventLocalProfileChanged())
|
||||
updateGui()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
disposable += rxBus
|
||||
.toObservable(EventAutotuneUpdateGui::class.java)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
updateGui()
|
||||
}, { fabricPrivacy.logException(it) })
|
||||
checkNewDay()
|
||||
binding.tuneDays.value = autotunePlugin.lastNbDays.toDouble()
|
||||
updateGui()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateGui() {
|
||||
_binding ?: return
|
||||
profileStore = activePlugin.activeProfileSource.profile ?: ProfileStore(injector, JSONObject(), dateUtil)
|
||||
profileName = if (binding.profileList.text.toString() == rh.gs(R.string.active)) "" else binding.profileList.text.toString()
|
||||
profileFunction.getProfile()?.let { currentProfile ->
|
||||
profile = ATProfile(profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(it) } ?:currentProfile, LocalInsulin(""), injector)
|
||||
}
|
||||
val profileList: ArrayList<CharSequence> = profileStore.getProfileList()
|
||||
profileList.add(0, rh.gs(R.string.active))
|
||||
context?.let { context ->
|
||||
binding.profileList.setAdapter(ArrayAdapter(context, R.layout.spinner_centered, profileList))
|
||||
} ?: return
|
||||
// set selected to actual profile
|
||||
if (autotunePlugin.selectedProfile.isNotEmpty())
|
||||
binding.profileList.setText(autotunePlugin.selectedProfile, false)
|
||||
else {
|
||||
binding.profileList.setText(profileList[0], false)
|
||||
}
|
||||
binding.autotuneRun.visibility = View.GONE
|
||||
binding.autotuneCheckInputProfile.visibility = View.GONE
|
||||
binding.autotuneCopylocal.visibility = View.GONE
|
||||
binding.autotuneUpdateProfile.visibility = View.GONE
|
||||
binding.autotuneRevertProfile.visibility = View.GONE
|
||||
binding.autotuneProfileswitch.visibility = View.GONE
|
||||
binding.autotuneCompare.visibility = View.GONE
|
||||
if (autotunePlugin.calculationRunning) {
|
||||
binding.tuneWarning.text = rh.gs(R.string.autotune_warning_during_run)
|
||||
} else if (autotunePlugin.lastRunSuccess) {
|
||||
binding.autotuneCopylocal.visibility = View.VISIBLE
|
||||
binding.autotuneUpdateProfile.visibility = autotunePlugin.updateButtonVisibility
|
||||
binding.autotuneRevertProfile.visibility = if (autotunePlugin.updateButtonVisibility == View.VISIBLE) View.GONE else View.VISIBLE
|
||||
binding.autotuneProfileswitch.visibility = View.VISIBLE
|
||||
binding.autotuneCompare.visibility = View.VISIBLE
|
||||
binding.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run)
|
||||
} else {
|
||||
binding.autotuneRun.visibility = View.VISIBLE
|
||||
binding.autotuneCheckInputProfile.visibility = View.VISIBLE
|
||||
}
|
||||
binding.tuneLastrun.text = dateUtil.dateAndTimeString(autotunePlugin.lastRun)
|
||||
showResults()
|
||||
}
|
||||
|
||||
private fun checkNewDay() {
|
||||
val runToday = autotunePlugin.lastRun > MidnightTime.calc(dateUtil.now() - autotunePlugin.autotuneStartHour * 3600 * 1000L) + autotunePlugin.autotuneStartHour * 3600 * 1000L
|
||||
if (runToday && autotunePlugin.result != "")
|
||||
{
|
||||
binding.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run)
|
||||
} else if (!runToday || autotunePlugin.result.isNullOrEmpty()) { //if new day reinit result, default days, warning and button's visibility
|
||||
resetParam(!runToday)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addWarnings(): String {
|
||||
var warning = ""
|
||||
var nl = ""
|
||||
if (profileFunction.getProfile() == null) {
|
||||
warning = rh.gs(R.string.profileswitch_ismissing)
|
||||
return warning
|
||||
}
|
||||
profileFunction.getProfile()?.let {
|
||||
if (!profile.isValid) return rh.gs(R.string.autotune_profile_invalid)
|
||||
if (profile.icSize > 1) {
|
||||
warning += nl + rh.gs(R.string.autotune_ic_warning, profile.icSize, profile.ic)
|
||||
nl = "\n"
|
||||
}
|
||||
if (profile.isfSize > 1) {
|
||||
warning += nl + rh.gs(R.string.autotune_isf_warning, profile.isfSize, Profile.fromMgdlToUnits(profile.isf, profileFunction.getUnits()), profileFunction.getUnits().asText)
|
||||
}
|
||||
}
|
||||
return warning
|
||||
}
|
||||
|
||||
private fun resetParam(resetDay: Boolean = true) {
|
||||
binding.tuneWarning.text = addWarnings()
|
||||
if (resetDay)
|
||||
autotunePlugin.lastNbDays = sp.getInt(R.string.key_autotune_default_tune_days, 5).toString()
|
||||
autotunePlugin.result = ""
|
||||
binding.autotuneResults.removeAllViews()
|
||||
autotunePlugin.tunedProfile = null
|
||||
autotunePlugin.lastRunSuccess = false
|
||||
autotunePlugin.updateButtonVisibility = View.GONE
|
||||
}
|
||||
|
||||
private val textWatcher = object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) { updateGui() }
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
if (!binding.tuneDays.text.isEmpty()) {
|
||||
try {
|
||||
if (autotunePlugin.calculationRunning)
|
||||
binding.tuneDays.value = autotunePlugin.lastNbDays.toDouble()
|
||||
if (binding.tuneDays.value != autotunePlugin.lastNbDays.toDouble()) {
|
||||
autotunePlugin.lastNbDays = binding.tuneDays.text
|
||||
resetParam(false)
|
||||
}
|
||||
} catch (e:Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showResults() {
|
||||
Thread {
|
||||
runOnUiThread {
|
||||
binding.autotuneResults.removeAllViews()
|
||||
if (autotunePlugin.result.isNotBlank()) {
|
||||
var toMgDl = 1.0
|
||||
if (profileFunction.getUnits() == GlucoseUnit.MMOL) toMgDl = Constants.MMOLL_TO_MGDL
|
||||
binding.autotuneResults.addView(
|
||||
TableLayout(context).also { layout ->
|
||||
layout.addView(
|
||||
TextView(context).apply {
|
||||
text = autotunePlugin.result
|
||||
setTypeface(typeface, Typeface.BOLD)
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
setTextAppearance(android.R.style.TextAppearance_Material_Medium)
|
||||
})
|
||||
autotunePlugin.tunedProfile?.let { tuned ->
|
||||
layout.addView(toTableRowHeader())
|
||||
layout.addView(toTableRowValue(rh.gs(R.string.isf_short), Round.roundTo(autotunePlugin.pumpProfile.isf / toMgDl, 0.001), Round.roundTo(tuned.isf / toMgDl, 0.001)))
|
||||
layout.addView(toTableRowValue(rh.gs(R.string.ic_short), Round.roundTo(autotunePlugin.pumpProfile.ic, 0.001), Round.roundTo(tuned.ic, 0.001)))
|
||||
layout.addView(
|
||||
TextView(context).apply {
|
||||
text = rh.gs(R.string.basal)
|
||||
setTypeface(typeface, Typeface.BOLD)
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
setTextAppearance(android.R.style.TextAppearance_Material_Medium)
|
||||
}
|
||||
)
|
||||
layout.addView(toTableRowHeader(true))
|
||||
var totalPump = 0.0
|
||||
var totalTuned = 0.0
|
||||
for (h in 0 until tuned.basal.size) {
|
||||
val df = DecimalFormat("00")
|
||||
val time = df.format(h.toLong()) + ":00"
|
||||
totalPump += autotunePlugin.pumpProfile.basal[h]
|
||||
totalTuned += tuned.basal[h]
|
||||
layout.addView(toTableRowValue(time, autotunePlugin.pumpProfile.basal[h], tuned.basal[h], tuned.basalUntuned[h].toString()))
|
||||
}
|
||||
layout.addView(toTableRowValue("∑", totalPump, totalTuned, " "))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.autotuneResultsCard.visibility = if (autotunePlugin.calculationRunning && autotunePlugin.result.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun toTableRowHeader(basal:Boolean = false): TableRow =
|
||||
TableRow(context).also { header ->
|
||||
val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT).apply { weight = 1f }
|
||||
header.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL }
|
||||
header.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 0 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = if (basal) rh.gs(R.string.time) else rh.gs(R.string.autotune_param)
|
||||
})
|
||||
header.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 1 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = rh.gs(R.string.profile)
|
||||
})
|
||||
header.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 2 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = rh.gs(R.string.autotune_tunedprofile_name)
|
||||
})
|
||||
header.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 3 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = rh.gs(R.string.autotune_percent)
|
||||
})
|
||||
header.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 4 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = if (basal) rh.gs(R.string.autotune_missing) else " "
|
||||
})
|
||||
}
|
||||
|
||||
fun toTableRowValue(hour: String, inputValue: Double, tunedValue: Double, missing: String = ""): TableRow =
|
||||
TableRow(context).also { row ->
|
||||
val percentValue = Round.roundTo(tunedValue / inputValue * 100 - 100, 1.0).toInt().toString() + "%"
|
||||
val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT).apply { weight = 1f }
|
||||
row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL }
|
||||
row.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 0 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = hour
|
||||
})
|
||||
row.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 1 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = String.format("%.3f", inputValue)
|
||||
})
|
||||
row.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 2 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = String.format("%.3f", tunedValue)
|
||||
})
|
||||
row.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 3 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = percentValue
|
||||
})
|
||||
row.addView(TextView(context).apply {
|
||||
layoutParams = lp.apply { column = 4 }
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
text = missing
|
||||
})
|
||||
}
|
||||
|
||||
private fun log(message: String) {
|
||||
autotuneFS.atLog("[Fragment] $message")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune
|
||||
|
||||
import info.nightscout.androidaps.Constants
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.data.*
|
||||
import info.nightscout.androidaps.database.AppRepository
|
||||
import info.nightscout.androidaps.database.embedments.InterfaceIDs
|
||||
import info.nightscout.androidaps.interfaces.ActivePlugin
|
||||
import info.nightscout.androidaps.interfaces.ProfileFunction
|
||||
import info.nightscout.androidaps.database.entities.*
|
||||
import info.nightscout.androidaps.extensions.durationInMinutes
|
||||
import info.nightscout.androidaps.extensions.iobCalc
|
||||
import info.nightscout.androidaps.extensions.toJson
|
||||
import info.nightscout.androidaps.extensions.toTemporaryBasal
|
||||
import info.nightscout.androidaps.interfaces.Profile
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.Round
|
||||
import info.nightscout.androidaps.utils.T
|
||||
import info.nightscout.shared.logging.AAPSLogger
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Singleton
|
||||
class AutotuneIob @Inject constructor(
|
||||
private val aapsLogger: AAPSLogger,
|
||||
private val repository: AppRepository,
|
||||
private val profileFunction: ProfileFunction,
|
||||
private val sp: SP,
|
||||
private val dateUtil: DateUtil,
|
||||
private val activePlugin: ActivePlugin,
|
||||
private val autotuneFS: AutotuneFS
|
||||
) {
|
||||
|
||||
private val nsTreatments = ArrayList<NsTreatment>()
|
||||
private var dia: Double = Constants.defaultDIA
|
||||
var boluses: MutableList<Bolus> = ArrayList()
|
||||
var meals = ArrayList<Carbs>()
|
||||
lateinit var glucose: List<GlucoseValue> // newest at index 0
|
||||
private lateinit var tempBasals: MutableList<TemporaryBasal>
|
||||
var startBG: Long = 0
|
||||
var endBG: Long = 0
|
||||
private fun range(): Long = (60 * 60 * 1000L * dia + T.hours(2).msecs()).toLong()
|
||||
|
||||
fun initializeData(from: Long, to: Long, tunedProfile: ATProfile) {
|
||||
dia = tunedProfile.dia
|
||||
startBG = from
|
||||
endBG = to
|
||||
nsTreatments.clear()
|
||||
tempBasals = ArrayList<TemporaryBasal>()
|
||||
initializeBgreadings(from, to)
|
||||
initializeTreatmentData(from - range(), to)
|
||||
initializeTempBasalData(from - range(), to, tunedProfile)
|
||||
initializeExtendedBolusData(from - range(), to, tunedProfile)
|
||||
Collections.sort(tempBasals) { o1: TemporaryBasal, o2: TemporaryBasal -> (o2.timestamp - o1.timestamp).toInt() }
|
||||
// Without Neutral TBR, Autotune Web will ignore iob for periods without TBR running
|
||||
addNeutralTempBasal(from - range(), to, tunedProfile)
|
||||
Collections.sort(nsTreatments) { o1: NsTreatment, o2: NsTreatment -> (o2.date - o1.date).toInt() }
|
||||
Collections.sort(boluses) { o1: Bolus, o2: Bolus -> (o2.timestamp - o1.timestamp).toInt() }
|
||||
log.debug("D/AutotunePlugin: Nb Treatments: " + nsTreatments.size + " Nb meals: " + meals.size)
|
||||
}
|
||||
|
||||
private fun initializeBgreadings(from: Long, to: Long) {
|
||||
glucose = repository.compatGetBgReadingsDataFromTime(from, to, false).blockingGet();
|
||||
}
|
||||
|
||||
//nsTreatment is used only for export data, meals is used in AutotunePrep
|
||||
private fun initializeTreatmentData(from: Long, to: Long) {
|
||||
val oldestBgDate = if (glucose.size > 0) glucose[glucose.size - 1].timestamp else from
|
||||
log.debug("AutotunePlugin Check BG date: BG Size: " + glucose.size + " OldestBG: " + dateUtil.dateAndTimeAndSecondsString(oldestBgDate) + " to: " + dateUtil.dateAndTimeAndSecondsString(to))
|
||||
val tmpCarbs = repository.getCarbsDataFromTimeToTimeExpanded(from, to, false).blockingGet()
|
||||
log.debug("AutotunePlugin Nb treatments after query: " + tmpCarbs.size)
|
||||
meals.clear()
|
||||
boluses.clear()
|
||||
var nbCarbs = 0
|
||||
for (i in tmpCarbs.indices) {
|
||||
val tp = tmpCarbs[i]
|
||||
if (tp.isValid) {
|
||||
nsTreatments.add(NsTreatment(tp))
|
||||
//only carbs after first BGReadings are taken into account in calculation of Autotune
|
||||
if (tp.amount > 0.0 && tp.timestamp >= oldestBgDate) meals.add(tmpCarbs[i])
|
||||
if (tp.timestamp < to && tp.amount > 0.0)
|
||||
nbCarbs++
|
||||
}
|
||||
}
|
||||
val tmpBolus = repository.getBolusesDataFromTimeToTime(from, to, false).blockingGet()
|
||||
var nbSMB = 0
|
||||
var nbBolus = 0
|
||||
for (i in tmpBolus.indices) {
|
||||
val tp = tmpBolus[i]
|
||||
if (tp.isValid && tp.type != Bolus.Type.PRIMING) {
|
||||
boluses.add(tp)
|
||||
nsTreatments.add(NsTreatment(tp))
|
||||
//only carbs after first BGReadings are taken into account in calculation of Autotune
|
||||
if (tp.timestamp < to) {
|
||||
if (tp.type == Bolus.Type.SMB)
|
||||
nbSMB++
|
||||
else if (tp.amount > 0.0)
|
||||
nbBolus++
|
||||
}
|
||||
}
|
||||
}
|
||||
//log.debug("AutotunePlugin Nb Meals: $nbCarbs Nb Bolus: $nbBolus Nb SMB: $nbSMB")
|
||||
}
|
||||
|
||||
//nsTreatment is used only for export data
|
||||
private fun initializeTempBasalData(from: Long, to: Long, tunedProfile: ATProfile) {
|
||||
val tBRs = repository.getTemporaryBasalsDataFromTimeToTime(from, to, false).blockingGet()
|
||||
//log.debug("D/AutotunePlugin tempBasal size before cleaning:" + tBRs.size);
|
||||
for (i in tBRs.indices) {
|
||||
if (tBRs[i].isValid)
|
||||
toSplittedTimestampTB(tBRs[i], tunedProfile)
|
||||
}
|
||||
//log.debug("D/AutotunePlugin: tempBasal size: " + tempBasals.size)
|
||||
}
|
||||
|
||||
//nsTreatment is used only for export data
|
||||
private fun initializeExtendedBolusData(from: Long, to: Long, tunedProfile: ATProfile) {
|
||||
val extendedBoluses = repository.getExtendedBolusDataFromTimeToTime(from, to, false).blockingGet()
|
||||
val pumpInterface = activePlugin.activePump
|
||||
if (pumpInterface.isFakingTempsByExtendedBoluses) {
|
||||
for (i in extendedBoluses.indices) {
|
||||
val eb = extendedBoluses[i]
|
||||
if (eb.isValid)
|
||||
profileFunction.getProfile(eb.timestamp)?.let {
|
||||
toSplittedTimestampTB(eb.toTemporaryBasal(it), tunedProfile)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i in extendedBoluses.indices) {
|
||||
val eb = extendedBoluses[i]
|
||||
if (eb.isValid) {
|
||||
nsTreatments.add(NsTreatment(eb))
|
||||
boluses.addAll(convertToBoluses(eb))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addNeutralTempBasal will add a fake neutral TBR (100%) to have correct basal rate in exported file for periods without TBR running
|
||||
// to be able to compare results between oref0 algo and aaps
|
||||
private fun addNeutralTempBasal(from: Long, to: Long, tunedProfile: ATProfile) {
|
||||
var previousStart = to
|
||||
for (i in tempBasals.indices) {
|
||||
val newStart = tempBasals[i].timestamp + tempBasals[i].duration
|
||||
if (previousStart - newStart > T.mins(1).msecs()) { // fill neutral only if more than 1 min
|
||||
val neutralTbr = TemporaryBasal(
|
||||
isValid = true,
|
||||
isAbsolute = false,
|
||||
timestamp = newStart,
|
||||
rate = 100.0,
|
||||
duration = previousStart - newStart,
|
||||
interfaceIDs_backing = InterfaceIDs(nightscoutId = "neutral_" + newStart.toString()),
|
||||
type = TemporaryBasal.Type.NORMAL
|
||||
)
|
||||
toSplittedTimestampTB(neutralTbr, tunedProfile)
|
||||
}
|
||||
previousStart = tempBasals[i].timestamp
|
||||
}
|
||||
if (previousStart - from > T.mins(1).msecs()) { // fill neutral only if more than 1 min
|
||||
val neutralTbr = TemporaryBasal(
|
||||
isValid = true,
|
||||
isAbsolute = false,
|
||||
timestamp = from,
|
||||
rate = 100.0,
|
||||
duration = previousStart - from,
|
||||
interfaceIDs_backing = InterfaceIDs(nightscoutId = "neutral_" + from.toString()),
|
||||
type = TemporaryBasal.Type.NORMAL
|
||||
)
|
||||
toSplittedTimestampTB(neutralTbr, tunedProfile)
|
||||
}
|
||||
}
|
||||
|
||||
// toSplittedTimestampTB will split all TBR across hours in different TBR with correct absolute value to be sure to have correct basal rate
|
||||
// even if profile rate is not the same
|
||||
private fun toSplittedTimestampTB(tb: TemporaryBasal, tunedProfile: ATProfile) {
|
||||
var splittedTimestamp = tb.timestamp
|
||||
val cutInMilliSec = T.mins(30).msecs() //30 min to compare with oref0
|
||||
var splittedDuration = tb.duration
|
||||
if (tb.isValid && tb.durationInMinutes > 0) {
|
||||
val endTimestamp = splittedTimestamp + splittedDuration
|
||||
while (splittedDuration > 0) {
|
||||
if (Profile.milliSecFromMidnight(splittedTimestamp) / cutInMilliSec == Profile.milliSecFromMidnight(endTimestamp) / cutInMilliSec) {
|
||||
val newtb = TemporaryBasal(
|
||||
isValid = true,
|
||||
isAbsolute = tb.isAbsolute,
|
||||
timestamp = splittedTimestamp,
|
||||
rate = tb.rate,
|
||||
duration = splittedDuration,
|
||||
interfaceIDs_backing = tb.interfaceIDs_backing,
|
||||
type = tb.type
|
||||
)
|
||||
tempBasals.add(newtb)
|
||||
nsTreatments.add(NsTreatment(newtb))
|
||||
splittedDuration = 0
|
||||
val profile = profileFunction.getProfile(newtb.timestamp) ?:continue
|
||||
boluses.addAll(convertToBoluses(newtb, profile, tunedProfile.profile)) //
|
||||
// required for correct iob calculation with oref0 algo
|
||||
} else {
|
||||
val durationFilled = (cutInMilliSec - Profile.milliSecFromMidnight(splittedTimestamp) % cutInMilliSec)
|
||||
val newtb = TemporaryBasal(
|
||||
isValid = true,
|
||||
isAbsolute = tb.isAbsolute,
|
||||
timestamp = splittedTimestamp,
|
||||
rate = tb.rate,
|
||||
duration = durationFilled,
|
||||
interfaceIDs_backing = tb.interfaceIDs_backing,
|
||||
type = tb.type
|
||||
)
|
||||
tempBasals.add(newtb)
|
||||
nsTreatments.add(NsTreatment(newtb))
|
||||
splittedTimestamp += durationFilled
|
||||
splittedDuration = splittedDuration - durationFilled
|
||||
val profile = profileFunction.getProfile(newtb.timestamp) ?:continue
|
||||
boluses.addAll(convertToBoluses(newtb, profile, tunedProfile.profile)) // required for correct iob calculation with oref0 algo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getIOB(time: Long, localInsulin: LocalInsulin): IobTotal {
|
||||
val bolusIob = getCalculationToTimeTreatments(time, localInsulin).round()
|
||||
return bolusIob
|
||||
}
|
||||
|
||||
fun getCalculationToTimeTreatments(time: Long, localInsulin: LocalInsulin): IobTotal {
|
||||
val total = IobTotal(time)
|
||||
val detailedLog = sp.getBoolean(R.string.key_autotune_additional_log, false)
|
||||
for (pos in boluses.indices) {
|
||||
val t = boluses[pos]
|
||||
if (!t.isValid) continue
|
||||
if (t.timestamp > time || t.timestamp < time - localInsulin.duration) continue
|
||||
val tIOB = t.iobCalc(time, localInsulin)
|
||||
if (detailedLog)
|
||||
log("iobCalc;${t.interfaceIDs.nightscoutId};$time;${t.timestamp};${tIOB.iobContrib};${tIOB.activityContrib};${dateUtil.dateAndTimeAndSecondsString(time)};${dateUtil.dateAndTimeAndSecondsString(t.timestamp)}")
|
||||
total.iob += tIOB.iobContrib
|
||||
total.activity += tIOB.activityContrib
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
|
||||
fun convertToBoluses(eb: ExtendedBolus): MutableList<Bolus> {
|
||||
val result: MutableList<Bolus> = ArrayList()
|
||||
val tempBolusSize = 0.05
|
||||
val tempBolusCount : Int = (eb.amount / tempBolusSize).roundToInt()
|
||||
if(tempBolusCount > 0) {
|
||||
val tempBolusSpacing = eb.duration / tempBolusCount
|
||||
for (j in 0L until tempBolusCount) {
|
||||
val calcDate = eb.timestamp + j * tempBolusSpacing
|
||||
val bolusInterfaceIDs = InterfaceIDs().also { it.nightscoutId = eb.interfaceIDs.nightscoutId + "_eb_$j" }
|
||||
val tempBolusPart = Bolus(
|
||||
interfaceIDs_backing = bolusInterfaceIDs,
|
||||
timestamp = calcDate,
|
||||
amount = tempBolusSize,
|
||||
type = Bolus.Type.NORMAL
|
||||
)
|
||||
result.add(tempBolusPart)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun convertToBoluses(tbr: TemporaryBasal, profile: Profile, tunedProfile: Profile): MutableList<Bolus> {
|
||||
val result: MutableList<Bolus> = ArrayList()
|
||||
val realDuration = tbr.durationInMinutes
|
||||
val basalRate = profile.getBasal(tbr.timestamp)
|
||||
val tunedRate = tunedProfile.getBasal(tbr.timestamp)
|
||||
val netBasalRate = Round.roundTo(if (tbr.isAbsolute) {
|
||||
tbr.rate - tunedRate
|
||||
} else {
|
||||
tbr.rate / 100.0 * basalRate - tunedRate
|
||||
}, 0.001)
|
||||
val tempBolusSize = if (netBasalRate < 0 ) -0.05 else 0.05
|
||||
val netBasalAmount: Double = Round.roundTo(netBasalRate * realDuration / 60.0, 0.01)
|
||||
val tempBolusCount : Int = (netBasalAmount / tempBolusSize).roundToInt()
|
||||
if(tempBolusCount > 0) {
|
||||
val tempBolusSpacing = realDuration * 60 * 1000 / tempBolusCount
|
||||
for (j in 0L until tempBolusCount) {
|
||||
val calcDate = tbr.timestamp + j * tempBolusSpacing
|
||||
val bolusInterfaceIDs = InterfaceIDs().also { it.nightscoutId = tbr.interfaceIDs.nightscoutId + "_tbr_$j" }
|
||||
val tempBolusPart = Bolus(
|
||||
interfaceIDs_backing = bolusInterfaceIDs,
|
||||
timestamp = calcDate,
|
||||
amount = tempBolusSize,
|
||||
type = Bolus.Type.NORMAL
|
||||
)
|
||||
result.add(tempBolusPart)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
fun glucoseToJSON(): String {
|
||||
val glucoseJson = JSONArray()
|
||||
for (bgreading in glucose)
|
||||
glucoseJson.put(bgreading.toJson(true, dateUtil))
|
||||
return glucoseJson.toString(2)
|
||||
}
|
||||
|
||||
fun bolusesToJSON(): String {
|
||||
val bolusesJson = JSONArray()
|
||||
for (bolus in boluses)
|
||||
bolusesJson.put(bolus.toJson(true, dateUtil))
|
||||
return bolusesJson.toString(2)
|
||||
}
|
||||
|
||||
fun nsHistoryToJSON(): String {
|
||||
val json = JSONArray()
|
||||
for (t in nsTreatments) {
|
||||
json.put(t.toJson())
|
||||
}
|
||||
return json.toString(2).replace("\\/", "/")
|
||||
}
|
||||
|
||||
//I add this internal class to be able to export easily ns-treatment files with same contain and format than NS query used by oref0-autotune
|
||||
private inner class NsTreatment {
|
||||
|
||||
var date: Long = 0
|
||||
var eventType: TherapyEvent.Type? = null
|
||||
var carbsTreatment: Carbs? = null
|
||||
var bolusTreatment: Bolus? = null
|
||||
var temporaryBasal: TemporaryBasal? = null
|
||||
var extendedBolus: ExtendedBolus? = null
|
||||
|
||||
constructor(t: Carbs) {
|
||||
carbsTreatment = t
|
||||
date = t.timestamp
|
||||
eventType = TherapyEvent.Type.CARBS_CORRECTION
|
||||
}
|
||||
|
||||
constructor(t: Bolus) {
|
||||
bolusTreatment = t
|
||||
date = t.timestamp
|
||||
eventType = TherapyEvent.Type.CORRECTION_BOLUS
|
||||
}
|
||||
|
||||
constructor(t: TemporaryBasal) {
|
||||
temporaryBasal = t
|
||||
date = t.timestamp
|
||||
eventType = TherapyEvent.Type.TEMPORARY_BASAL
|
||||
}
|
||||
|
||||
constructor(t: ExtendedBolus) {
|
||||
extendedBolus = t
|
||||
date = t.timestamp
|
||||
eventType = TherapyEvent.Type.COMBO_BOLUS
|
||||
}
|
||||
|
||||
fun toJson(): JSONObject? {
|
||||
val cPjson = JSONObject()
|
||||
return when (eventType) {
|
||||
TherapyEvent.Type.TEMPORARY_BASAL ->
|
||||
temporaryBasal?.let { tbr ->
|
||||
val profile = profileFunction.getProfile(tbr.timestamp)
|
||||
profile?.let {
|
||||
tbr.toJson(true, it, dateUtil)
|
||||
}
|
||||
}
|
||||
TherapyEvent.Type.COMBO_BOLUS ->
|
||||
extendedBolus?.let {
|
||||
val profile = profileFunction.getProfile(it.timestamp)
|
||||
it.toJson(true, profile!!, dateUtil)
|
||||
}
|
||||
TherapyEvent.Type.CORRECTION_BOLUS -> bolusTreatment?.toJson(true, dateUtil)
|
||||
TherapyEvent.Type.CARBS_CORRECTION -> carbsTreatment?.toJson(true, dateUtil)
|
||||
else -> cPjson
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun log(message: String) {
|
||||
autotuneFS.atLog("[iob] $message")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AutotunePlugin::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune
|
||||
|
||||
import android.view.View
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.data.LocalInsulin
|
||||
import info.nightscout.androidaps.data.ProfileSealed
|
||||
import info.nightscout.androidaps.database.entities.UserEntry
|
||||
import info.nightscout.androidaps.database.entities.ValueWithUnit
|
||||
import info.nightscout.androidaps.interfaces.*
|
||||
import info.nightscout.androidaps.logging.UserEntryLogger
|
||||
import info.nightscout.androidaps.plugins.bus.RxBus
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
|
||||
import info.nightscout.androidaps.plugins.general.autotune.events.EventAutotuneUpdateGui
|
||||
import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin
|
||||
import info.nightscout.androidaps.plugins.profile.local.events.EventLocalProfileChanged
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.MidnightTime
|
||||
import info.nightscout.androidaps.utils.T
|
||||
import info.nightscout.shared.logging.AAPSLogger
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* adaptation from oref0 autotune started by philoul on 2020 (complete refactoring of AutotunePlugin initialised by Rumen Georgiev on 1/29/2018.)
|
||||
*
|
||||
* TODO: replace Thread by Worker
|
||||
* TODO: future version (once first version validated): add DIA and Peak tune for insulin
|
||||
* TODO: future version: Allow day of the week selection to tune specifics days (training days, working days, WE days)
|
||||
*/
|
||||
|
||||
@Singleton
|
||||
class AutotunePlugin @Inject constructor(
|
||||
injector: HasAndroidInjector,
|
||||
resourceHelper: ResourceHelper,
|
||||
private val sp: SP,
|
||||
private val rxBus: RxBus,
|
||||
private val profileFunction: ProfileFunction,
|
||||
private val dateUtil: DateUtil,
|
||||
private val activePlugin: ActivePlugin,
|
||||
private val localProfilePlugin: LocalProfilePlugin,
|
||||
private val autotuneFS: AutotuneFS,
|
||||
private val autotuneIob: AutotuneIob,
|
||||
private val autotunePrep: AutotunePrep,
|
||||
private val autotuneCore: AutotuneCore,
|
||||
private val uel: UserEntryLogger,
|
||||
aapsLogger: AAPSLogger
|
||||
) : PluginBase(PluginDescription()
|
||||
.mainType(PluginType.GENERAL)
|
||||
.fragmentClass(AutotuneFragment::class.qualifiedName)
|
||||
.pluginIcon(R.drawable.ic_autotune)
|
||||
.pluginName(R.string.autotune)
|
||||
.shortName(R.string.autotune_shortname)
|
||||
.preferencesId(R.xml.pref_autotune)
|
||||
.description(R.string.autotune_description),
|
||||
aapsLogger, resourceHelper, injector
|
||||
), Autotune {
|
||||
@Volatile override var lastRunSuccess: Boolean = false
|
||||
@Volatile var result: String = ""
|
||||
@Volatile var calculationRunning: Boolean = false
|
||||
@Volatile var lastRun: Long = 0
|
||||
@Volatile var selectedProfile = ""
|
||||
@Volatile var lastNbDays: String = ""
|
||||
@Volatile var updateButtonVisibility: Int = 0
|
||||
@Volatile lateinit var pumpProfile: ATProfile
|
||||
@Volatile var tunedProfile: ATProfile? = null
|
||||
private var preppedGlucose: PreppedGlucose? = null
|
||||
private lateinit var profile: Profile
|
||||
val autotuneStartHour: Int = 4
|
||||
|
||||
override fun aapsAutotune(daysBack: Int, autoSwitch: Boolean, profileToTune: String): String {
|
||||
tunedProfile = null
|
||||
updateButtonVisibility = View.GONE
|
||||
lastRunSuccess = false
|
||||
var logResult = ""
|
||||
result = ""
|
||||
if (profileFunction.getProfile() == null) {
|
||||
result = rh.gs(R.string.profileswitch_ismissing)
|
||||
return result
|
||||
}
|
||||
val detailedLog = sp.getBoolean(R.string.key_autotune_additional_log, false)
|
||||
calculationRunning = true
|
||||
lastNbDays = "" + daysBack
|
||||
lastRun = dateUtil.now()
|
||||
val profileStore = activePlugin.activeProfileSource.profile ?: return rh.gs(R.string.profileswitch_ismissing)
|
||||
selectedProfile = if (profileToTune.isEmpty()) profileFunction.getProfileName() else profileToTune
|
||||
profileFunction.getProfile()?.let { currentProfile ->
|
||||
profile = profileStore.getSpecificProfile(profileToTune)?.let { ProfileSealed.Pure(it) } ?: currentProfile
|
||||
}
|
||||
var localInsulin = LocalInsulin("PumpInsulin", activePlugin.activeInsulin.peak, profile.dia) // var because localInsulin could be updated later with Tune Insulin peak/dia
|
||||
|
||||
log("Start Autotune with $daysBack days back")
|
||||
autotuneFS.createAutotuneFolder() //create autotune subfolder for autotune files if not exists
|
||||
autotuneFS.deleteAutotuneFiles() //clean autotune folder before run
|
||||
// Today at 4 AM
|
||||
var endTime = MidnightTime.calc(lastRun) + autotuneStartHour * 60 * 60 * 1000L
|
||||
if (endTime > lastRun) endTime -= 24 * 60 * 60 * 1000L // Check if 4 AM is before now
|
||||
val starttime = endTime - daysBack * 24 * 60 * 60 * 1000L
|
||||
autotuneFS.exportSettings(settings(lastRun, daysBack, starttime, endTime))
|
||||
tunedProfile = ATProfile(profile, localInsulin, injector).also {
|
||||
it.profilename = rh.gs(R.string.autotune_tunedprofile_name)
|
||||
}
|
||||
pumpProfile = ATProfile(profile, localInsulin, injector).also {
|
||||
it.profilename = selectedProfile
|
||||
}
|
||||
autotuneFS.exportPumpProfile(pumpProfile)
|
||||
|
||||
for (i in 0 until daysBack) {
|
||||
val from = starttime + i * 24 * 60 * 60 * 1000L // get 24 hours BG values from 4 AM to 4 AM next day
|
||||
val to = from + 24 * 60 * 60 * 1000L
|
||||
log("Tune day " + (i + 1) + " of " + daysBack)
|
||||
tunedProfile?.let { tunedProfile ->
|
||||
autotuneIob.initializeData(from, to, tunedProfile) //autotuneIob contains BG and Treatments data from history (<=> query for ns-treatments and ns-entries)
|
||||
autotuneFS.exportEntries(autotuneIob) //<=> ns-entries.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine
|
||||
autotuneFS.exportTreatments(autotuneIob) //<=> ns-treatments.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine (include treatments ,tempBasal and extended
|
||||
preppedGlucose = autotunePrep.categorizeBGDatums(tunedProfile, localInsulin) //<=> autotune.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine
|
||||
}
|
||||
|
||||
if (preppedGlucose == null || tunedProfile == null) {
|
||||
result = rh.gs(R.string.autotune_error)
|
||||
log(result)
|
||||
calculationRunning = false
|
||||
rxBus.send(EventAutotuneUpdateGui())
|
||||
tunedProfile = null
|
||||
autotuneFS.exportResult(result)
|
||||
autotuneFS.exportLogAndZip(lastRun)
|
||||
return result
|
||||
}
|
||||
preppedGlucose?.let { preppedGlucose -> //preppedGlucose and tunedProfile should never be null here
|
||||
autotuneFS.exportPreppedGlucose(preppedGlucose)
|
||||
tunedProfile = autotuneCore.tuneAllTheThings(preppedGlucose, tunedProfile!!, pumpProfile)
|
||||
}
|
||||
// localInsulin = LocalInsulin("TunedInsulin", tunedProfile!!.peak, tunedProfile!!.dia) // Todo: Add tune Insulin option
|
||||
autotuneFS.exportTunedProfile(tunedProfile!!) //<=> newprofile.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine
|
||||
if (i < daysBack - 1) {
|
||||
log("Partial result for day ${i + 1}".trimIndent())
|
||||
result = rh.gs(R.string.autotune_partial_result, i + 1, daysBack)
|
||||
rxBus.send(EventAutotuneUpdateGui())
|
||||
}
|
||||
logResult = showResults(tunedProfile, pumpProfile)
|
||||
if (detailedLog)
|
||||
autotuneFS.exportLog(lastRun, i + 1)
|
||||
}
|
||||
result = rh.gs(R.string.autotune_result, dateUtil.dateAndTimeString(lastRun))
|
||||
if (!detailedLog)
|
||||
autotuneFS.exportLog(lastRun)
|
||||
autotuneFS.exportResult(logResult)
|
||||
autotuneFS.zipAutotune(lastRun)
|
||||
updateButtonVisibility = View.VISIBLE
|
||||
|
||||
if (autoSwitch) {
|
||||
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
|
||||
tunedProfile?.let { tunedP ->
|
||||
tunedP.profilename = pumpProfile.profilename
|
||||
updateProfile(tunedP)
|
||||
uel.log(
|
||||
UserEntry.Action.STORE_PROFILE,
|
||||
UserEntry.Sources.Autotune,
|
||||
ValueWithUnit.SimpleString(tunedP.profilename)
|
||||
)
|
||||
updateButtonVisibility = View.GONE
|
||||
tunedP.profileStore(circadian)?.let { profilestore ->
|
||||
if (profileFunction.createProfileSwitch(
|
||||
profilestore,
|
||||
profileName = tunedP.profilename,
|
||||
durationInMinutes = 0,
|
||||
percentage = 100,
|
||||
timeShiftInHours = 0,
|
||||
timestamp = dateUtil.now()
|
||||
)
|
||||
) {
|
||||
log("Profile Switch succeed ${tunedP.profilename}")
|
||||
uel.log(
|
||||
UserEntry.Action.PROFILE_SWITCH,
|
||||
UserEntry.Sources.Autotune,
|
||||
"Autotune AutoSwitch",
|
||||
ValueWithUnit.SimpleString(tunedP.profilename))
|
||||
}
|
||||
rxBus.send(EventLocalProfileChanged())
|
||||
}
|
||||
}
|
||||
}
|
||||
lastRunSuccess = true
|
||||
sp.putLong(R.string.key_autotune_last_run, lastRun)
|
||||
rxBus.send(EventAutotuneUpdateGui())
|
||||
calculationRunning = false
|
||||
tunedProfile?.let {
|
||||
return result
|
||||
}
|
||||
return "No Result" // should never occurs
|
||||
}
|
||||
|
||||
private fun showResults(tunedProfile: ATProfile?, pumpProfile: ATProfile): String {
|
||||
if (tunedProfile == null)
|
||||
return "No Result" // should never occurs
|
||||
val line = rh.gs(R.string.autotune_log_separator)
|
||||
var strResult = line
|
||||
strResult += rh.gs(R.string.autotune_log_title)
|
||||
strResult += line
|
||||
// show ISF and CR
|
||||
strResult += rh.gs(R.string.autotune_log_isf, rh.gs(R.string.isf_short), pumpProfile.isf, tunedProfile.isf)
|
||||
strResult += rh.gs(R.string.autotune_log_ic, rh.gs(R.string.ic_short), pumpProfile.ic, tunedProfile.ic)
|
||||
strResult += line
|
||||
var totalBasal = 0.0
|
||||
var totalTuned = 0.0
|
||||
for (i in 0..23) {
|
||||
totalBasal += pumpProfile.basal[i]
|
||||
totalTuned += tunedProfile.basal[i]
|
||||
val percentageChangeValue = tunedProfile.basal[i] / pumpProfile.basal[i] * 100 - 100
|
||||
strResult += rh.gs(R.string.autotune_log_basal, i.toDouble(), pumpProfile.basal[i], tunedProfile.basal[i], tunedProfile.basalUntuned[i], percentageChangeValue)
|
||||
}
|
||||
strResult += line
|
||||
strResult += rh.gs(R.string.autotune_log_sum_basal, totalBasal, totalTuned)
|
||||
strResult += line
|
||||
log(strResult)
|
||||
return strResult
|
||||
}
|
||||
|
||||
private fun settings(runDate: Long, nbDays: Int, firstloopstart: Long, lastloopend: Long): String {
|
||||
var jsonString = ""
|
||||
val jsonSettings = JSONObject()
|
||||
val insulinInterface = activePlugin.activeInsulin
|
||||
val utcOffset = T.msecs(TimeZone.getDefault().getOffset(dateUtil.now()).toLong()).hours()
|
||||
val startDateString = dateUtil.toISOString(firstloopstart).substring(0,10)
|
||||
val endDateString = dateUtil.toISOString(lastloopend - 24 * 60 * 60 * 1000L).substring(0,10)
|
||||
val nsUrl = sp.getString(R.string.key_nsclientinternal_url, "")
|
||||
val optCategorizeUam = if (sp.getBoolean(R.string.key_autotune_categorize_uam_as_basal, false)) "-c=true" else ""
|
||||
val optInsulinCurve = ""
|
||||
try {
|
||||
jsonSettings.put("datestring", dateUtil.toISOString(runDate))
|
||||
jsonSettings.put("dateutc", dateUtil.toISOAsUTC(runDate))
|
||||
jsonSettings.put("utcOffset", utcOffset)
|
||||
jsonSettings.put("units", profileFunction.getUnits().asText)
|
||||
jsonSettings.put("timezone", TimeZone.getDefault().id)
|
||||
jsonSettings.put("url_nightscout", sp.getString(R.string.key_nsclientinternal_url, ""))
|
||||
jsonSettings.put("nbdays", nbDays)
|
||||
jsonSettings.put("startdate", startDateString)
|
||||
jsonSettings.put("enddate", endDateString)
|
||||
// command to change timezone
|
||||
jsonSettings.put("timezone_command", "sudo ln -sf /usr/share/zoneinfo/" + TimeZone.getDefault().id + " /etc/localtime")
|
||||
// oref0_command is for running oref0-autotune on a virtual machine in a dedicated ~/aaps subfolder
|
||||
jsonSettings.put("oref0_command", "oref0-autotune -d=~/aaps -n=$nsUrl -s=$startDateString -e=$endDateString $optCategorizeUam $optInsulinCurve")
|
||||
// aaps_command is for running modified oref0-autotune with exported data from aaps (ns-entries and ns-treatment json files copied in ~/aaps/autotune folder and pumpprofile.json copied in ~/aaps/settings/
|
||||
jsonSettings.put("aaps_command", "aaps-autotune -d=~/aaps -s=$startDateString -e=$endDateString $optCategorizeUam $optInsulinCurve")
|
||||
jsonSettings.put("categorize_uam_as_basal", sp.getBoolean(R.string.key_autotune_categorize_uam_as_basal, false))
|
||||
jsonSettings.put("tune_insulin_curve", false)
|
||||
|
||||
val peaktime: Int = insulinInterface.peak
|
||||
if (insulinInterface.id === Insulin.InsulinType.OREF_ULTRA_RAPID_ACTING)
|
||||
jsonSettings.put("curve","ultra-rapid")
|
||||
else if (insulinInterface.id === Insulin.InsulinType.OREF_RAPID_ACTING)
|
||||
jsonSettings.put("curve", "rapid-acting")
|
||||
else if (insulinInterface.id === Insulin.InsulinType.OREF_LYUMJEV) {
|
||||
jsonSettings.put("curve", "ultra-rapid")
|
||||
jsonSettings.put("useCustomPeakTime", true)
|
||||
jsonSettings.put("insulinPeakTime", peaktime)
|
||||
} else if (insulinInterface.id === Insulin.InsulinType.OREF_FREE_PEAK) {
|
||||
jsonSettings.put("curve", if (peaktime > 55) "rapid-acting" else "ultra-rapid")
|
||||
jsonSettings.put("useCustomPeakTime", true)
|
||||
jsonSettings.put("insulinPeakTime", peaktime)
|
||||
}
|
||||
jsonString = jsonSettings.toString(4).replace("\\/", "/")
|
||||
} catch (e: JSONException) { }
|
||||
return jsonString
|
||||
}
|
||||
|
||||
fun updateProfile(newProfile: ATProfile?) {
|
||||
if (newProfile == null) return
|
||||
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
|
||||
val profileStore = activePlugin.activeProfileSource.profile ?: ProfileStore(injector, JSONObject(), dateUtil)
|
||||
val profileList: ArrayList<CharSequence> = profileStore.getProfileList()
|
||||
var indexLocalProfile = -1
|
||||
for (p in profileList.indices)
|
||||
if (profileList[p] == newProfile.profilename)
|
||||
indexLocalProfile = p
|
||||
if (indexLocalProfile == -1) {
|
||||
localProfilePlugin.addProfile(localProfilePlugin.copyFrom(newProfile.getProfile(circadian), newProfile.profilename))
|
||||
return
|
||||
}
|
||||
localProfilePlugin.currentProfileIndex = indexLocalProfile
|
||||
localProfilePlugin.currentProfile()?.dia = newProfile.dia
|
||||
localProfilePlugin.currentProfile()?.basal = newProfile.basal()
|
||||
localProfilePlugin.currentProfile()?.ic = newProfile.ic(circadian)
|
||||
localProfilePlugin.currentProfile()?.isf = newProfile.isf(circadian)
|
||||
localProfilePlugin.storeSettings()
|
||||
}
|
||||
|
||||
|
||||
private fun log(message: String) {
|
||||
atLog("[Plugin] $message")
|
||||
}
|
||||
|
||||
override fun atLog(message: String) {
|
||||
autotuneFS.atLog(message)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,558 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune
|
||||
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.data.LocalInsulin
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.BGDatum
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.CRDatum
|
||||
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
|
||||
import info.nightscout.androidaps.database.entities.Bolus
|
||||
import info.nightscout.androidaps.database.entities.Carbs
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.Round
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AutotunePrep @Inject constructor(
|
||||
private val sp: SP,
|
||||
private val dateUtil: DateUtil,
|
||||
private val autotuneFS: AutotuneFS,
|
||||
private val autotuneIob: AutotuneIob
|
||||
) {
|
||||
// private static Logger log = LoggerFactory.getLogger(AutotunePlugin.class);
|
||||
fun categorizeBGDatums(tunedprofile: ATProfile, localInsulin: LocalInsulin): PreppedGlucose? {
|
||||
//lib/meals is called before to get only meals data (in AAPS it's done in AutotuneIob)
|
||||
var treatments: MutableList<Carbs> = autotuneIob.meals
|
||||
var boluses: MutableList<Bolus> = autotuneIob.boluses
|
||||
// Bloc between #21 and # 54 replaced by bloc below (just remove BG value below 39, Collections.sort probably not necessary because BG values already sorted...)
|
||||
val glucose = autotuneIob.glucose
|
||||
val glucoseData: MutableList<GlucoseValue> = ArrayList()
|
||||
for (i in glucose.indices) {
|
||||
if (glucose[i].value > 39) {
|
||||
glucoseData.add(glucose[i])
|
||||
}
|
||||
}
|
||||
if (glucose.size == 0 || glucoseData.size == 0 ) {
|
||||
log("No BG value received")
|
||||
return null
|
||||
}
|
||||
|
||||
glucoseData.sortWith(object: Comparator<GlucoseValue>{ override fun compare(o1: GlucoseValue, o2: GlucoseValue): Int = (o2.timestamp - o1.timestamp).toInt() })
|
||||
|
||||
// Bloc below replace bloc between #55 and #71
|
||||
// boluses and maxCarbs not used here ?,
|
||||
// IOBInputs are for iob calculation (done here in AutotuneIob Class)
|
||||
//val boluses = 0
|
||||
//val maxCarbs = 0
|
||||
if (treatments.size == 0) {
|
||||
log("No Carbs entries")
|
||||
//return null
|
||||
}
|
||||
if (autotuneIob.boluses.size == 0) {
|
||||
log("No treatment received")
|
||||
return null
|
||||
}
|
||||
|
||||
var csfGlucoseData: MutableList<BGDatum> = ArrayList()
|
||||
var isfGlucoseData: MutableList<BGDatum> = ArrayList()
|
||||
var basalGlucoseData: MutableList<BGDatum> = ArrayList()
|
||||
val uamGlucoseData: MutableList<BGDatum> = ArrayList()
|
||||
val crData: MutableList<CRDatum> = ArrayList()
|
||||
|
||||
//Bloc below replace bloc between #72 and #93
|
||||
// I keep it because BG lines in log are consistent between AAPS and Oref0
|
||||
val bucketedData: MutableList<BGDatum> = ArrayList()
|
||||
bucketedData.add(BGDatum(glucoseData[0], dateUtil))
|
||||
//int j=0;
|
||||
var k = 0 // index of first value used by bucket
|
||||
//for loop to validate and bucket the data
|
||||
for (i in 1 until glucoseData.size) {
|
||||
val BGTime = glucoseData[i].timestamp
|
||||
val lastBGTime = glucoseData[k].timestamp
|
||||
val elapsedMinutes = (BGTime - lastBGTime) / (60 * 1000)
|
||||
if (Math.abs(elapsedMinutes) >= 2) {
|
||||
//j++; // move to next bucket
|
||||
k = i // store index of first value used by bucket
|
||||
bucketedData.add(BGDatum(glucoseData[i], dateUtil))
|
||||
} else {
|
||||
// average all readings within time deadband
|
||||
val average = glucoseData[k]
|
||||
for (l in k + 1 until i + 1) {
|
||||
average.value += glucoseData[l].value
|
||||
}
|
||||
average.value = average.value / (i - k + 1)
|
||||
bucketedData.add(BGDatum(average, dateUtil))
|
||||
}
|
||||
}
|
||||
|
||||
// Here treatments contains only meals data
|
||||
// bloc between #94 and #114 remove meals before first BG value
|
||||
|
||||
// Bloc below replace bloc between #115 and #122 (initialize data before main loop)
|
||||
// crInitialxx are declaration to be able to use these data in whole loop
|
||||
var calculatingCR = false
|
||||
var absorbing = false
|
||||
var uam = false // unannounced meal
|
||||
var mealCOB = 0.0
|
||||
var mealCarbs = 0.0
|
||||
var crCarbs = 0.0
|
||||
var type = ""
|
||||
var crInitialIOB = 0.0
|
||||
var crInitialBG = 0.0
|
||||
var crInitialCarbTime = 0L
|
||||
|
||||
//categorize.js#123 (Note: don't need fullHistory because data are managed in AutotuneIob Class)
|
||||
//Here is main loop between #125 and #366
|
||||
// main for loop
|
||||
for (i in bucketedData.size - 5 downTo 1) {
|
||||
val glucoseDatum = bucketedData[i]
|
||||
//log.debug(glucoseDatum);
|
||||
val BGTime = glucoseDatum.date
|
||||
|
||||
// As we're processing each data point, go through the treatment.carbs and see if any of them are older than
|
||||
// the current BG data point. If so, add those carbs to COB.
|
||||
val treatment = if (treatments.size > 0) treatments[treatments.size - 1] else null
|
||||
var myCarbs = 0.0
|
||||
if (treatment != null) {
|
||||
if (treatment.timestamp < BGTime) {
|
||||
if (treatment.amount > 0.0) {
|
||||
mealCOB += treatment.amount
|
||||
mealCarbs += treatment.amount
|
||||
myCarbs = treatment.amount
|
||||
}
|
||||
treatments.removeAt(treatments.size - 1)
|
||||
}
|
||||
}
|
||||
var bg = 0.0
|
||||
var avgDelta = 0.0
|
||||
|
||||
// TODO: re-implement interpolation to avoid issues here with gaps
|
||||
// calculate avgDelta as last 4 datapoints to better catch more rises after COB hits zero
|
||||
if (bucketedData[i].value != 0.0 && bucketedData[i + 4].value != 0.0) {
|
||||
//log.debug(bucketedData[i]);
|
||||
bg = bucketedData[i].value
|
||||
if (bg < 40 || bucketedData[i + 4].value < 40) {
|
||||
//process.stderr.write("!");
|
||||
continue
|
||||
}
|
||||
avgDelta = (bg - bucketedData[i + 4].value) / 4
|
||||
} else {
|
||||
log("Could not find glucose data")
|
||||
}
|
||||
avgDelta = Round.roundTo(avgDelta, 0.01)
|
||||
glucoseDatum.avgDelta = avgDelta
|
||||
|
||||
//sens = ISF
|
||||
val sens = tunedprofile.isf
|
||||
|
||||
// for IOB calculations, use the average of the last 4 hours' basals to help convergence;
|
||||
// this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge.
|
||||
// use the pumpbasalprofile to properly calculate IOB during periods where no temp basal is set
|
||||
/* Note Philoul currentPumpBasal never used in oref0 Autotune code
|
||||
var currentPumpBasal = pumpprofile.profile.getBasal(BGTime)
|
||||
currentPumpBasal += pumpprofile.profile.getBasal(BGTime - 1 * 60 * 60 * 1000)
|
||||
currentPumpBasal += pumpprofile.profile.getBasal(BGTime - 2 * 60 * 60 * 1000)
|
||||
currentPumpBasal += pumpprofile.profile.getBasal(BGTime - 3 * 60 * 60 * 1000)
|
||||
currentPumpBasal = Round.roundTo(currentPumpBasal / 4, 0.001) //CurrentPumpBasal for iob calculation is average of 4 last pumpProfile Basal rate
|
||||
*/
|
||||
// this is the current autotuned basal, used for everything else besides IOB calculations
|
||||
val currentBasal = tunedprofile.getBasal(BGTime)
|
||||
|
||||
// basalBGI is BGI of basal insulin activity.
|
||||
val basalBGI = Round.roundTo(currentBasal * sens / 60 * 5, 0.01) // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m
|
||||
//console.log(JSON.stringify(IOBInputs.profile));
|
||||
// call iob since calculated elsewhere
|
||||
//var iob = getIOB(IOBInputs)[0];
|
||||
// in autotune iob is calculated with 6 hours of history data, tunedProfile and average pumpProfile basal rate...
|
||||
//log("currentBasal: " + currentBasal + " BGTime: " + BGTime + " / " + dateUtil!!.timeStringWithSeconds(BGTime) + "******************************************************************************************")
|
||||
val iob = autotuneIob.getIOB(BGTime, localInsulin) // add localInsulin to be independent to InsulinPlugin
|
||||
|
||||
// activity times ISF times 5 minutes is BGI
|
||||
val BGI = Round.roundTo(-iob.activity * sens * 5, 0.01)
|
||||
// datum = one glucose data point (being prepped to store in output)
|
||||
glucoseDatum.bgi = BGI
|
||||
// calculating deviation
|
||||
var deviation = avgDelta - BGI
|
||||
|
||||
// set positive deviations to zero if BG is below 80
|
||||
if (bg < 80 && deviation > 0) {
|
||||
deviation = 0.0
|
||||
}
|
||||
|
||||
// rounding and storing deviation
|
||||
deviation = Round.roundTo(deviation, 0.01)
|
||||
glucoseDatum.deviation = deviation
|
||||
|
||||
// Then, calculate carb absorption for that 5m interval using the deviation.
|
||||
if (mealCOB > 0) {
|
||||
val ci = Math.max(deviation, sp.getDouble("openapsama_min_5m_carbimpact", 3.0))
|
||||
val absorbed = ci * tunedprofile.ic / sens
|
||||
// Store the COB, and use it as the starting point for the next data point.
|
||||
mealCOB = Math.max(0.0, mealCOB - absorbed)
|
||||
}
|
||||
|
||||
// Calculate carb ratio (CR) independently of CSF and ISF
|
||||
// Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
|
||||
// For now, if another meal IOB/COB stacks on top of it, consider them together
|
||||
// Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
|
||||
// Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
|
||||
if (mealCOB > 0 || calculatingCR) {
|
||||
// set initial values when we first see COB
|
||||
crCarbs += myCarbs
|
||||
if (calculatingCR == false) {
|
||||
crInitialIOB = iob.iob
|
||||
crInitialBG = glucoseDatum.value
|
||||
crInitialCarbTime = glucoseDatum.date
|
||||
log("CRInitialIOB: " + crInitialIOB + " CRInitialBG: " + crInitialBG + " CRInitialCarbTime: " + dateUtil.toISOString(crInitialCarbTime))
|
||||
}
|
||||
// keep calculatingCR as long as we have COB or enough IOB
|
||||
if (mealCOB > 0 && i > 1) {
|
||||
calculatingCR = true
|
||||
} else if (iob.iob > currentBasal / 2 && i > 1) {
|
||||
calculatingCR = true
|
||||
// when COB=0 and IOB drops low enough, record end values and be done calculatingCR
|
||||
} else {
|
||||
val crEndIOB = iob.iob
|
||||
val crEndBG = glucoseDatum.value
|
||||
val crEndTime = glucoseDatum.date
|
||||
log("CREndIOB: " + crEndIOB + " CREndBG: " + crEndBG + " CREndTime: " + dateUtil.toISOString(crEndTime))
|
||||
val crDatum = CRDatum(dateUtil)
|
||||
crDatum.crInitialBG = crInitialBG
|
||||
crDatum.crInitialIOB = crInitialIOB
|
||||
crDatum.crInitialCarbTime = crInitialCarbTime
|
||||
crDatum.crEndBG = crEndBG
|
||||
crDatum.crEndIOB = crEndIOB
|
||||
crDatum.crEndTime = crEndTime
|
||||
crDatum.crCarbs = crCarbs
|
||||
//log.debug(CRDatum);
|
||||
//String crDataString = "{\"CRInitialIOB\": " + CRInitialIOB + ", \"CRInitialBG\": " + CRInitialBG + ", \"CRInitialCarbTime\": " + CRInitialCarbTime + ", \"CREndIOB\": " + CREndIOB + ", \"CREndBG\": " + CREndBG + ", \"CREndTime\": " + CREndTime + ", \"CRCarbs\": " + CRCarbs + "}";
|
||||
val CRElapsedMinutes = Math.round((crEndTime - crInitialCarbTime) / (1000 * 60).toFloat())
|
||||
|
||||
//log.debug(CREndTime - CRInitialCarbTime, CRElapsedMinutes);
|
||||
if (CRElapsedMinutes < 60 || i == 1 && mealCOB > 0) {
|
||||
log("Ignoring $CRElapsedMinutes m CR period.")
|
||||
} else {
|
||||
crData.add(crDatum)
|
||||
}
|
||||
crCarbs = 0.0
|
||||
calculatingCR = false
|
||||
}
|
||||
}
|
||||
|
||||
// If mealCOB is zero but all deviations since hitting COB=0 are positive, assign those data points to CSFGlucoseData
|
||||
// Once deviations go negative for at least one data point after COB=0, we can use the rest of the data to tune ISF or basals
|
||||
if (mealCOB > 0 || absorbing || mealCarbs > 0) {
|
||||
// if meal IOB has decayed, then end absorption after this data point unless COB > 0
|
||||
absorbing = if (iob.iob < currentBasal / 2) {
|
||||
false
|
||||
// otherwise, as long as deviations are positive, keep tracking carb deviations
|
||||
} else if (deviation > 0) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if (!absorbing && mealCOB == 0.0) {
|
||||
mealCarbs = 0.0
|
||||
}
|
||||
// check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag
|
||||
//log.debug(type);
|
||||
if (type != "csf") {
|
||||
glucoseDatum.mealAbsorption = "start"
|
||||
log(glucoseDatum.mealAbsorption + " carb absorption")
|
||||
}
|
||||
type = "csf"
|
||||
glucoseDatum.mealCarbs = mealCarbs.toInt()
|
||||
//if (i == 0) { glucoseDatum.mealAbsorption = "end"; }
|
||||
csfGlucoseData.add(glucoseDatum)
|
||||
} else {
|
||||
// check previous "type" value, and if it was csf, set a mealAbsorption end flag
|
||||
if (type == "csf") {
|
||||
csfGlucoseData[csfGlucoseData.size - 1].mealAbsorption = "end"
|
||||
log(csfGlucoseData[csfGlucoseData.size - 1].mealAbsorption + " carb absorption")
|
||||
}
|
||||
if (iob.iob > 2 * currentBasal || deviation > 6 || uam) {
|
||||
uam = if (deviation > 0) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if (type != "uam") {
|
||||
glucoseDatum.uamAbsorption = "start"
|
||||
log(glucoseDatum.uamAbsorption + " unannnounced meal absorption")
|
||||
}
|
||||
type = "uam"
|
||||
uamGlucoseData.add(glucoseDatum)
|
||||
} else {
|
||||
if (type == "uam") {
|
||||
log("end unannounced meal absorption")
|
||||
}
|
||||
|
||||
// Go through the remaining time periods and divide them into periods where scheduled basal insulin activity dominates. This would be determined by calculating the BG impact of scheduled basal insulin
|
||||
// (for example 1U/hr * 48 mg/dL/U ISF = 48 mg/dL/hr = 5 mg/dL/5m), and comparing that to BGI from bolus and net basal insulin activity.
|
||||
// When BGI is positive (insulin activity is negative), we want to use that data to tune basals
|
||||
// When BGI is smaller than about 1/4 of basalBGI, we want to use that data to tune basals
|
||||
// When BGI is negative and more than about 1/4 of basalBGI, we can use that data to tune ISF,
|
||||
// unless avgDelta is positive: then that's some sort of unexplained rise we don't want to use for ISF, so that means basals
|
||||
if (basalBGI > -4 * BGI) {
|
||||
type = "basal"
|
||||
basalGlucoseData.add(glucoseDatum)
|
||||
} else {
|
||||
if (avgDelta > 0 && avgDelta > -2 * BGI) {
|
||||
//type="unknown"
|
||||
type = "basal"
|
||||
basalGlucoseData.add(glucoseDatum)
|
||||
} else {
|
||||
type = "ISF"
|
||||
isfGlucoseData.add(glucoseDatum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// debug line to print out all the things
|
||||
log((if (absorbing) 1 else 0).toString() + " mealCOB: " + Round.roundTo(mealCOB, 0.1) + " mealCarbs: " + Math.round(mealCarbs) + " basalBGI: " + Round.roundTo(basalBGI, 0.1) + " BGI: " + Round.roundTo(BGI, 0.1) + " IOB: " + iob.iob+ " Activity: " + iob.activity + " at " + dateUtil.timeStringWithSeconds(BGTime) + " dev: " + deviation + " avgDelta: " + avgDelta + " " + type)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************
|
||||
|
||||
// categorize.js Lines 372-383
|
||||
for (crDatum in crData) {
|
||||
crDatum.crInsulin = dosed(crDatum.crInitialCarbTime, crDatum.crEndTime, boluses)
|
||||
}
|
||||
// categorize.js Lines 384-436
|
||||
val CSFLength = csfGlucoseData.size
|
||||
var ISFLength = isfGlucoseData.size
|
||||
val UAMLength = uamGlucoseData.size
|
||||
var basalLength = basalGlucoseData.size
|
||||
if (sp.getBoolean(R.string.key_autotune_categorize_uam_as_basal, false)) {
|
||||
log("Categorizing all UAM data as basal.")
|
||||
basalGlucoseData.addAll(uamGlucoseData)
|
||||
} else if (CSFLength > 12) {
|
||||
log("Found at least 1h of carb: assuming meals were announced, and categorizing UAM data as basal.")
|
||||
basalGlucoseData.addAll(uamGlucoseData)
|
||||
} else {
|
||||
if (2 * basalLength < UAMLength) {
|
||||
//log.debug(basalGlucoseData, UAMGlucoseData);
|
||||
log("Warning: too many deviations categorized as UnAnnounced Meals")
|
||||
log("Adding $UAMLength UAM deviations to $basalLength basal ones")
|
||||
basalGlucoseData.addAll(uamGlucoseData)
|
||||
//log.debug(basalGlucoseData);
|
||||
// if too much data is excluded as UAM, add in the UAM deviations, but then discard the highest 50%
|
||||
basalGlucoseData.sortWith(object: Comparator<BGDatum>{ override fun compare(o1: BGDatum, o2: BGDatum): Int = (100 * o1.deviation - 100 * o2.deviation).toInt() }) //deviation rouded to 0.01, so *100 to avoid crash during sort
|
||||
val newBasalGlucose: MutableList<BGDatum> = ArrayList()
|
||||
for (i in 0 until basalGlucoseData.size / 2) {
|
||||
newBasalGlucose.add(basalGlucoseData[i])
|
||||
}
|
||||
//log.debug(newBasalGlucose);
|
||||
basalGlucoseData = newBasalGlucose
|
||||
log("and selecting the lowest 50%, leaving " + basalGlucoseData.size + " basal+UAM ones")
|
||||
}
|
||||
if (2 * ISFLength < UAMLength) {
|
||||
log("Adding $UAMLength UAM deviations to $ISFLength ISF ones")
|
||||
isfGlucoseData.addAll(uamGlucoseData)
|
||||
// if too much data is excluded as UAM, add in the UAM deviations to ISF, but then discard the highest 50%
|
||||
isfGlucoseData.sortWith(object: Comparator<BGDatum>{ override fun compare(o1: BGDatum, o2: BGDatum): Int = (100 * o1.deviation - 100 * o2.deviation).toInt() }) //deviation rouded to 0.01, so *100 to avoid crash during sort
|
||||
val newISFGlucose: MutableList<BGDatum> = ArrayList()
|
||||
for (i in 0 until isfGlucoseData.size / 2) {
|
||||
newISFGlucose.add(isfGlucoseData[i])
|
||||
}
|
||||
//console.error(newISFGlucose);
|
||||
isfGlucoseData = newISFGlucose
|
||||
log("and selecting the lowest 50%, leaving " + isfGlucoseData.size + " ISF+UAM ones")
|
||||
//log.error(ISFGlucoseData.length, UAMLength);
|
||||
}
|
||||
}
|
||||
basalLength = basalGlucoseData.size
|
||||
ISFLength = isfGlucoseData.size
|
||||
if (4 * basalLength + ISFLength < CSFLength && ISFLength < 10) {
|
||||
log("Warning: too many deviations categorized as meals")
|
||||
//log.debug("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones");
|
||||
//var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData);
|
||||
log("Adding $CSFLength CSF deviations to $ISFLength ISF ones")
|
||||
isfGlucoseData.addAll(csfGlucoseData)
|
||||
csfGlucoseData = ArrayList()
|
||||
}
|
||||
|
||||
// categorize.js Lines 437-444
|
||||
log("CRData: " + crData.size + " CSFGlucoseData: " + csfGlucoseData.size + " ISFGlucoseData: " + isfGlucoseData.size + " BasalGlucoseData: " + basalGlucoseData.size)
|
||||
// Here is the end of categorize.js file
|
||||
|
||||
/* bloc below is for --tune-insulin-curve not developed for the moment
|
||||
// these lines are in index.js file (autotune-prep folder)
|
||||
if (inputs.tune_insulin_curve) {
|
||||
if (opts.profile.curve === 'bilinear') {
|
||||
console.error('--tune-insulin-curve is set but only valid for exponential curves');
|
||||
} else {
|
||||
var minDeviations = 1000000;
|
||||
var newDIA = 0;
|
||||
var diaDeviations = [];
|
||||
var peakDeviations = [];
|
||||
var currentDIA = opts.profile.dia;
|
||||
var currentPeak = opts.profile.insulinPeakTime;
|
||||
|
||||
var consoleError = console.error;
|
||||
console.error = function() {};
|
||||
|
||||
var startDIA=currentDIA - 2;
|
||||
var endDIA=currentDIA + 2;
|
||||
for (var dia=startDIA; dia <= endDIA; ++dia) {
|
||||
var sqrtDeviations = 0;
|
||||
var deviations = 0;
|
||||
var deviationsSq = 0;
|
||||
|
||||
opts.profile.dia = dia;
|
||||
|
||||
var curve_output = categorize(opts);
|
||||
var basalGlucose = curve_output.basalGlucoseData;
|
||||
|
||||
for (var hour=0; hour < 24; ++hour) {
|
||||
for (var i=0; i < basalGlucose.length; ++i) {
|
||||
var BGTime;
|
||||
|
||||
if (basalGlucose[i].date) {
|
||||
BGTime = new Date(basalGlucose[i].date);
|
||||
} else if (basalGlucose[i].displayTime) {
|
||||
BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
|
||||
} else if (basalGlucose[i].dateString) {
|
||||
BGTime = new Date(basalGlucose[i].dateString);
|
||||
} else {
|
||||
consoleError("Could not determine last BG time");
|
||||
}
|
||||
|
||||
var myHour = BGTime.getHours();
|
||||
if (hour === myHour) {
|
||||
//console.error(basalGlucose[i].deviation);
|
||||
sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5);
|
||||
deviations += Math.abs(parseFloat(basalGlucose[i].deviation));
|
||||
deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var meanDeviation = Math.round(Math.abs(deviations/basalGlucose.length)*1000)/1000;
|
||||
var SMRDeviation = Math.round(Math.pow(sqrtDeviations/basalGlucose.length,2)*1000)/1000;
|
||||
var RMSDeviation = Math.round(Math.pow(deviationsSq/basalGlucose.length,0.5)*1000)/1000;
|
||||
consoleError('insulinEndTime', dia, 'meanDeviation:', meanDeviation, 'SMRDeviation:', SMRDeviation, 'RMSDeviation:',RMSDeviation, '(mg/dL)');
|
||||
diaDeviations.push({
|
||||
dia: dia,
|
||||
meanDeviation: meanDeviation,
|
||||
SMRDeviation: SMRDeviation,
|
||||
RMSDeviation: RMSDeviation,
|
||||
});
|
||||
autotune_prep_output.diaDeviations = diaDeviations;
|
||||
|
||||
deviations = Math.round(deviations*1000)/1000;
|
||||
if (deviations < minDeviations) {
|
||||
minDeviations = Math.round(deviations*1000)/1000;
|
||||
newDIA = dia;
|
||||
}
|
||||
}
|
||||
|
||||
// consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)');
|
||||
//consoleError(diaDeviations);
|
||||
|
||||
minDeviations = 1000000;
|
||||
|
||||
var newPeak = 0;
|
||||
opts.profile.dia = currentDIA;
|
||||
//consoleError(opts.profile.useCustomPeakTime, opts.profile.insulinPeakTime);
|
||||
if ( ! opts.profile.useCustomPeakTime === true && opts.profile.curve === "ultra-rapid" ) {
|
||||
opts.profile.insulinPeakTime = 55;
|
||||
} else if ( ! opts.profile.useCustomPeakTime === true ) {
|
||||
opts.profile.insulinPeakTime = 75;
|
||||
}
|
||||
opts.profile.useCustomPeakTime = true;
|
||||
|
||||
var startPeak=opts.profile.insulinPeakTime - 10;
|
||||
var endPeak=opts.profile.insulinPeakTime + 10;
|
||||
for (var peak=startPeak; peak <= endPeak; peak=(peak+5)) {
|
||||
sqrtDeviations = 0;
|
||||
deviations = 0;
|
||||
deviationsSq = 0;
|
||||
|
||||
opts.profile.insulinPeakTime = peak;
|
||||
|
||||
|
||||
curve_output = categorize(opts);
|
||||
basalGlucose = curve_output.basalGlucoseData;
|
||||
|
||||
for (hour=0; hour < 24; ++hour) {
|
||||
for (i=0; i < basalGlucose.length; ++i) {
|
||||
if (basalGlucose[i].date) {
|
||||
BGTime = new Date(basalGlucose[i].date);
|
||||
} else if (basalGlucose[i].displayTime) {
|
||||
BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
|
||||
} else if (basalGlucose[i].dateString) {
|
||||
BGTime = new Date(basalGlucose[i].dateString);
|
||||
} else {
|
||||
consoleError("Could not determine last BG time");
|
||||
}
|
||||
|
||||
myHour = BGTime.getHours();
|
||||
if (hour === myHour) {
|
||||
//console.error(basalGlucose[i].deviation);
|
||||
sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5);
|
||||
deviations += Math.abs(parseFloat(basalGlucose[i].deviation));
|
||||
deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error(deviationsSq);
|
||||
|
||||
meanDeviation = Math.round(deviations/basalGlucose.length*1000)/1000;
|
||||
SMRDeviation = Math.round(Math.pow(sqrtDeviations/basalGlucose.length,2)*1000)/1000;
|
||||
RMSDeviation = Math.round(Math.pow(deviationsSq/basalGlucose.length,0.5)*1000)/1000;
|
||||
consoleError('insulinPeakTime', peak, 'meanDeviation:', meanDeviation, 'SMRDeviation:', SMRDeviation, 'RMSDeviation:',RMSDeviation, '(mg/dL)');
|
||||
peakDeviations.push({
|
||||
peak: peak,
|
||||
meanDeviation: meanDeviation,
|
||||
SMRDeviation: SMRDeviation,
|
||||
RMSDeviation: RMSDeviation,
|
||||
});
|
||||
autotune_prep_output.diaDeviations = diaDeviations;
|
||||
|
||||
deviations = Math.round(deviations*1000)/1000;
|
||||
if (deviations < minDeviations) {
|
||||
minDeviations = Math.round(deviations*1000)/1000;
|
||||
newPeak = peak;
|
||||
}
|
||||
}
|
||||
|
||||
//consoleError('Optimum insulinPeakTime', newPeak, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)');
|
||||
//consoleError(peakDeviations);
|
||||
autotune_prep_output.peakDeviations = peakDeviations;
|
||||
|
||||
console.error = consoleError;
|
||||
}
|
||||
}
|
||||
*/
|
||||
return PreppedGlucose(autotuneIob.startBG, crData, csfGlucoseData, isfGlucoseData, basalGlucoseData, dateUtil)
|
||||
|
||||
// and may be later
|
||||
// return new PreppedGlucose(crData, csfGlucoseData, isfGlucoseData, basalGlucoseData, diaDeviations, peakDeviations);
|
||||
}
|
||||
|
||||
//dosed.js full
|
||||
private fun dosed(start: Long, end: Long, treatments: List<Bolus>): Double {
|
||||
var insulinDosed = 0.0
|
||||
if (treatments.size == 0) {
|
||||
log("No treatments to process.")
|
||||
return 0.0
|
||||
}
|
||||
for (treatment in treatments) {
|
||||
if (treatment.amount != 0.0 && treatment.timestamp > start && treatment.timestamp <= end) {
|
||||
insulinDosed += treatment.amount
|
||||
//log("CRDATA;${dateUtil.toISOString(start)};${dateUtil.toISOString(end)};${treatment.timestamp};${treatment.amount};$insulinDosed")
|
||||
}
|
||||
}
|
||||
//log("insulin dosed: " + insulinDosed);
|
||||
return Round.roundTo(insulinDosed, 0.001)
|
||||
}
|
||||
|
||||
private fun log(message: String) {
|
||||
autotuneFS.atLog("[Prep] $message")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.data
|
||||
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.core.R
|
||||
import info.nightscout.androidaps.data.LocalInsulin
|
||||
import info.nightscout.androidaps.data.ProfileSealed
|
||||
import info.nightscout.androidaps.data.PureProfile
|
||||
import info.nightscout.androidaps.database.data.Block
|
||||
import info.nightscout.androidaps.extensions.blockValueBySeconds
|
||||
import info.nightscout.androidaps.extensions.pureProfileFromJson
|
||||
import info.nightscout.androidaps.interfaces.*
|
||||
import info.nightscout.androidaps.plugins.bus.RxBus
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.Round
|
||||
import info.nightscout.androidaps.utils.T
|
||||
import info.nightscout.shared.SafeParse
|
||||
import info.nightscout.shared.sharedPreferences.SP
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.text.DecimalFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class ATProfile(profile: Profile, var localInsulin: LocalInsulin, val injector: HasAndroidInjector) {
|
||||
|
||||
@Inject lateinit var activePlugin: ActivePlugin
|
||||
@Inject lateinit var sp: SP
|
||||
@Inject lateinit var profileFunction: ProfileFunction
|
||||
@Inject lateinit var dateUtil: DateUtil
|
||||
@Inject lateinit var config: Config
|
||||
@Inject lateinit var rxBus: RxBus
|
||||
@Inject lateinit var rh: ResourceHelper
|
||||
|
||||
var profile: ProfileSealed
|
||||
var circadianProfile: ProfileSealed
|
||||
lateinit var pumpProfile: ProfileSealed
|
||||
var profilename: String = ""
|
||||
var basal = DoubleArray(24)
|
||||
var basalUntuned = IntArray(24)
|
||||
var ic = 0.0
|
||||
var isf = 0.0
|
||||
var dia = 0.0
|
||||
var peak = 0
|
||||
var isValid: Boolean = false
|
||||
var from: Long = 0
|
||||
var pumpProfileAvgISF = 0.0
|
||||
var pumpProfileAvgIC = 0.0
|
||||
|
||||
val icSize: Int
|
||||
get() = profile.getIcsValues().size
|
||||
val isfSize: Int
|
||||
get() = profile.getIsfsMgdlValues().size
|
||||
val avgISF: Double
|
||||
get() = if (profile.getIsfsMgdlValues().size == 1) profile.getIsfsMgdlValues().get(0).value else Round.roundTo(averageProfileValue(profile.getIsfsMgdlValues()), 0.01)
|
||||
val avgIC: Double
|
||||
get() = if (profile.getIcsValues().size == 1) profile.getIcsValues().get(0).value else Round.roundTo(averageProfileValue(profile.getIcsValues()), 0.01)
|
||||
|
||||
fun getBasal(timestamp: Long): Double = basal[Profile.secondsFromMidnight(timestamp)/3600]
|
||||
|
||||
// for localProfilePlugin Synchronisation
|
||||
fun basal() = jsonArray(basal)
|
||||
fun ic(circadian: Boolean = false): JSONArray {
|
||||
if(circadian)
|
||||
return jsonArray(pumpProfile.icBlocks, avgIC/pumpProfileAvgIC)
|
||||
return jsonArray(ic)
|
||||
}
|
||||
fun isf(circadian: Boolean = false): JSONArray {
|
||||
if(circadian)
|
||||
return jsonArray(pumpProfile.isfBlocks, avgISF/pumpProfileAvgISF)
|
||||
return jsonArray(Profile.fromMgdlToUnits(isf, profile.units))
|
||||
}
|
||||
|
||||
fun getProfile(circadian: Boolean = false): PureProfile {
|
||||
return if (circadian)
|
||||
circadianProfile.convertToNonCustomizedProfile(dateUtil)
|
||||
else
|
||||
profile.convertToNonCustomizedProfile(dateUtil)
|
||||
}
|
||||
|
||||
fun updateProfile() {
|
||||
data()?.let { profile = ProfileSealed.Pure(it) }
|
||||
data(true)?.let { circadianProfile = ProfileSealed.Pure(it) }
|
||||
}
|
||||
|
||||
//Export json string with oref0 format used for autotune
|
||||
// Include min_5m_carbimpact, insulin type, single value for carb_ratio and isf
|
||||
fun profiletoOrefJSON(): String {
|
||||
var jsonString = ""
|
||||
val json = JSONObject()
|
||||
val insulinInterface: Insulin = activePlugin.activeInsulin
|
||||
try {
|
||||
json.put("name", profilename)
|
||||
json.put("min_5m_carbimpact", sp.getDouble("openapsama_min_5m_carbimpact", 3.0))
|
||||
json.put("dia", dia)
|
||||
if (insulinInterface.id === Insulin.InsulinType.OREF_ULTRA_RAPID_ACTING) json.put(
|
||||
"curve",
|
||||
"ultra-rapid"
|
||||
) else if (insulinInterface.id === Insulin.InsulinType.OREF_RAPID_ACTING) json.put("curve", "rapid-acting") else if (insulinInterface.id === Insulin.InsulinType.OREF_LYUMJEV) {
|
||||
json.put("curve", "ultra-rapid")
|
||||
json.put("useCustomPeakTime", true)
|
||||
json.put("insulinPeakTime", 45)
|
||||
} else if (insulinInterface.id === Insulin.InsulinType.OREF_FREE_PEAK) {
|
||||
val peaktime: Int = sp.getInt(rh.gs(R.string.key_insulin_oref_peak), 75)
|
||||
json.put("curve", if (peaktime > 50) "rapid-acting" else "ultra-rapid")
|
||||
json.put("useCustomPeakTime", true)
|
||||
json.put("insulinPeakTime", peaktime)
|
||||
}
|
||||
val basals = JSONArray()
|
||||
for (h in 0..23) {
|
||||
val secondfrommidnight = h * 60 * 60
|
||||
var time: String
|
||||
time = DecimalFormat("00").format(h) + ":00:00"
|
||||
basals.put(
|
||||
JSONObject()
|
||||
.put("start", time)
|
||||
.put("minutes", h * 60)
|
||||
.put("rate", profile.getBasalTimeFromMidnight(secondfrommidnight)
|
||||
)
|
||||
)
|
||||
}
|
||||
json.put("basalprofile", basals)
|
||||
val isfvalue = Round.roundTo(avgISF, 0.001)
|
||||
json.put(
|
||||
"isfProfile",
|
||||
JSONObject().put(
|
||||
"sensitivities",
|
||||
JSONArray().put(JSONObject().put("i", 0).put("start", "00:00:00").put("sensitivity", isfvalue).put("offset", 0).put("x", 0).put("endoffset", 1440))
|
||||
)
|
||||
)
|
||||
json.put("carb_ratio", avgIC)
|
||||
json.put("autosens_max", SafeParse.stringToDouble(sp.getString(R.string.key_openapsama_autosens_max, "1.2")))
|
||||
json.put("autosens_min", SafeParse.stringToDouble(sp.getString(R.string.key_openapsama_autosens_min, "0.7")))
|
||||
json.put("units", GlucoseUnit.MGDL.asText)
|
||||
json.put("timezone", TimeZone.getDefault().id)
|
||||
jsonString = json.toString(2).replace("\\/", "/")
|
||||
} catch (e: JSONException) {}
|
||||
|
||||
return jsonString
|
||||
}
|
||||
|
||||
fun data(circadian: Boolean = false): PureProfile? {
|
||||
val json: JSONObject = profile.toPureNsJson(dateUtil)
|
||||
try {
|
||||
json.put("dia", dia)
|
||||
if (circadian) {
|
||||
json.put("sens", jsonArray(pumpProfile.isfBlocks, avgISF/pumpProfileAvgISF))
|
||||
json.put("carbratio", jsonArray(pumpProfile.icBlocks, avgIC/pumpProfileAvgIC))
|
||||
} else {
|
||||
json.put("sens", jsonArray(Profile.fromMgdlToUnits(isf, profile.units)))
|
||||
json.put("carbratio", jsonArray(ic))
|
||||
}
|
||||
json.put("basal", jsonArray(basal))
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
return pureProfileFromJson(json, dateUtil, profile.units.asText)
|
||||
}
|
||||
|
||||
fun profileStore(circadian: Boolean = false): ProfileStore?
|
||||
{
|
||||
var profileStore: ProfileStore? = null
|
||||
val json = JSONObject()
|
||||
val store = JSONObject()
|
||||
val tunedProfile = if (circadian) circadianProfile else profile
|
||||
if (profilename.isEmpty())
|
||||
profilename = rh.gs(R.string.autotune_tunedprofile_name)
|
||||
try {
|
||||
store.put(profilename, tunedProfile.toPureNsJson(dateUtil))
|
||||
json.put("defaultProfile", profilename)
|
||||
json.put("store", store)
|
||||
json.put("startDate", dateUtil.toISOAsUTC(dateUtil.now()))
|
||||
profileStore = ProfileStore(injector, json, dateUtil)
|
||||
} catch (e: JSONException) { }
|
||||
return profileStore
|
||||
}
|
||||
|
||||
fun jsonArray(values: DoubleArray): JSONArray {
|
||||
val json = JSONArray()
|
||||
for (h in 0..23) {
|
||||
val secondfrommidnight = h * 60 * 60
|
||||
val df = DecimalFormat("00")
|
||||
val time = df.format(h.toLong()) + ":00"
|
||||
json.put(
|
||||
JSONObject()
|
||||
.put("time", time)
|
||||
.put("timeAsSeconds", secondfrommidnight)
|
||||
.put("value", values[h])
|
||||
)
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
fun jsonArray(value: Double) =
|
||||
JSONArray().put(
|
||||
JSONObject()
|
||||
.put("time", "00:00")
|
||||
.put("timeAsSeconds", 0)
|
||||
.put("value", value)
|
||||
)
|
||||
|
||||
fun jsonArray(values: List<Block>, multiplier: Double = 1.0): JSONArray {
|
||||
val json = JSONArray()
|
||||
var elapsedHours = 0L
|
||||
values.forEach {
|
||||
val value = values.blockValueBySeconds(T.hours(elapsedHours).secs().toInt(), multiplier, 0)
|
||||
json.put(
|
||||
JSONObject()
|
||||
.put("time", DecimalFormat("00").format(elapsedHours) + ":00")
|
||||
.put("timeAsSeconds", T.hours(elapsedHours).secs())
|
||||
.put("value", value)
|
||||
)
|
||||
elapsedHours += T.msecs(it.duration).hours()
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun averageProfileValue(pf: Array<Profile.ProfileValue>?): Double {
|
||||
var avgValue = 0.0
|
||||
val secondPerDay = 24 * 60 * 60
|
||||
if (pf == null) return avgValue
|
||||
for (i in pf.indices) {
|
||||
avgValue += pf[i].value * ((if (i == pf.size - 1) secondPerDay else pf[i + 1].timeAsSeconds) - pf[i].timeAsSeconds)
|
||||
}
|
||||
avgValue /= secondPerDay.toDouble()
|
||||
return avgValue
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
injector.androidInjector().inject(this)
|
||||
this.profile = profile as ProfileSealed
|
||||
circadianProfile = profile
|
||||
isValid = profile.isValid
|
||||
if (isValid) {
|
||||
//initialize tuned value with current profile values
|
||||
for (h in 0..23) {
|
||||
basal[h] = Round.roundTo(profile.basalBlocks.blockValueBySeconds(T.hours(h.toLong()).secs().toInt(), 1.0, 0), 0.001)
|
||||
}
|
||||
ic = avgIC
|
||||
isf = avgISF
|
||||
pumpProfile = profile
|
||||
pumpProfileAvgIC = avgIC
|
||||
pumpProfileAvgISF = avgISF
|
||||
}
|
||||
dia = localInsulin.dia
|
||||
peak = localInsulin.peak
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.data
|
||||
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue.TrendArrow
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import info.nightscout.androidaps.utils.T
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by Rumen Georgiev on 2/24/2018.
|
||||
*/
|
||||
class BGDatum {
|
||||
//Added by Rumen for autotune
|
||||
var id: Long = 0
|
||||
var date = 0L
|
||||
var value = 0.0
|
||||
var direction: TrendArrow? = null
|
||||
var deviation = 0.0
|
||||
var bgi = 0.0
|
||||
var mealAbsorption = ""
|
||||
var mealCarbs = 0
|
||||
var uamAbsorption = ""
|
||||
var avgDelta = 0.0
|
||||
var bgReading: GlucoseValue? = null
|
||||
private set
|
||||
var dateUtil: DateUtil
|
||||
|
||||
constructor(dateUtil: DateUtil) { this.dateUtil = dateUtil}
|
||||
constructor(json: JSONObject, dateUtil: DateUtil) {
|
||||
this.dateUtil = dateUtil
|
||||
try {
|
||||
if (json.has("_id")) id = json.getLong("_id")
|
||||
if (json.has("date")) date = json.getLong("date")
|
||||
if (json.has("sgv")) value = json.getDouble("sgv")
|
||||
if (json.has("direction")) direction = TrendArrow.fromString(json.getString("direction"))
|
||||
if (json.has("deviation")) deviation = json.getDouble("deviation")
|
||||
if (json.has("BGI")) bgi = json.getDouble("BGI")
|
||||
if (json.has("avgDelta")) avgDelta = json.getDouble("avgDelta")
|
||||
if (json.has("mealAbsorption")) mealAbsorption = json.getString("mealAbsorption")
|
||||
if (json.has("mealCarbs")) mealCarbs = json.getInt("mealCarbs")
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(glucoseValue: GlucoseValue, dateUtil: DateUtil) {
|
||||
this.dateUtil = dateUtil
|
||||
date = glucoseValue.timestamp
|
||||
value = glucoseValue.value
|
||||
direction = glucoseValue.trendArrow
|
||||
id = glucoseValue.id
|
||||
this.bgReading = glucoseValue
|
||||
}
|
||||
|
||||
fun toJSON(mealData: Boolean): JSONObject {
|
||||
val bgjson = JSONObject()
|
||||
val utcOffset = T.msecs(TimeZone.getDefault().getOffset(dateUtil.now()).toLong()).hours()
|
||||
try {
|
||||
bgjson.put("_id", id)
|
||||
bgjson.put("date", date)
|
||||
bgjson.put("dateString", dateUtil.toISOAsUTC(date))
|
||||
bgjson.put("sgv", value)
|
||||
bgjson.put("direction", direction)
|
||||
bgjson.put("type", "sgv")
|
||||
bgjson.put("sysTime", dateUtil.toISOAsUTC(date))
|
||||
bgjson.put("utcOffset", utcOffset)
|
||||
bgjson.put("glucose", value)
|
||||
bgjson.put("avgDelta", avgDelta)
|
||||
bgjson.put("BGI", bgi)
|
||||
bgjson.put("deviation", deviation)
|
||||
if (mealData) {
|
||||
bgjson.put("mealAbsorption", mealAbsorption)
|
||||
bgjson.put("mealCarbs", mealCarbs)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
return bgjson
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.data
|
||||
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Created by Rumen Georgiev on 2/26/2018.
|
||||
*/
|
||||
class CRDatum {
|
||||
|
||||
var crInitialIOB = 0.0
|
||||
var crInitialBG = 0.0
|
||||
var crInitialCarbTime = 0L
|
||||
var crEndIOB = 0.0
|
||||
var crEndBG = 0.0
|
||||
var crEndTime = 0L
|
||||
var crCarbs = 0.0
|
||||
var crInsulin = 0.0
|
||||
var crInsulinTotal = 0.0
|
||||
var dateUtil: DateUtil
|
||||
|
||||
constructor(dateUtil: DateUtil) { this.dateUtil = dateUtil}
|
||||
constructor(json: JSONObject, dateUtil: DateUtil) {
|
||||
this.dateUtil = dateUtil
|
||||
try {
|
||||
if (json.has("CRInitialIOB")) crInitialIOB = json.getDouble("CRInitialIOB")
|
||||
if (json.has("CRInitialBG")) crInitialBG = json.getDouble("CRInitialBG")
|
||||
if (json.has("CRInitialCarbTime")) crInitialCarbTime = dateUtil.fromISODateString(json.getString("CRInitialCarbTime"))
|
||||
if (json.has("CREndIOB")) crEndIOB = json.getDouble("CREndIOB")
|
||||
if (json.has("CREndBG")) crEndBG = json.getDouble("CREndBG")
|
||||
if (json.has("CREndTime")) crEndTime = dateUtil.fromISODateString(json.getString("CREndTime"))
|
||||
if (json.has("CRCarbs")) crCarbs = json.getDouble("CRCarbs")
|
||||
if (json.has("CRInsulin")) crInsulin = json.getDouble("CRInsulin")
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val crjson = JSONObject()
|
||||
try {
|
||||
crjson.put("CRInitialIOB", crInitialIOB)
|
||||
crjson.put("CRInitialBG", crInitialBG.toInt())
|
||||
crjson.put("CRInitialCarbTime", dateUtil.toISOString(crInitialCarbTime))
|
||||
crjson.put("CREndIOB", crEndIOB)
|
||||
crjson.put("CREndBG", crEndBG.toInt())
|
||||
crjson.put("CREndTime", dateUtil.toISOString(crEndTime))
|
||||
crjson.put("CRCarbs", crCarbs.toInt())
|
||||
crjson.put("CRInsulin", crInsulin)
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
return crjson
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.data
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class DiaDatum {
|
||||
|
||||
var dia = 0.0
|
||||
var meanDeviation = 0.0
|
||||
var smrDeviation = 0.0
|
||||
var rmsDeviation = 0.0
|
||||
|
||||
constructor() {}
|
||||
constructor(json: JSONObject) {
|
||||
try {
|
||||
if (json.has("dia")) dia = json.getDouble("dia")
|
||||
if (json.has("meanDeviation")) meanDeviation = json.getDouble("meanDeviation")
|
||||
if (json.has("SMRDeviation")) smrDeviation = json.getDouble("SMRDeviation")
|
||||
if (json.has("RMSDeviation")) rmsDeviation = json.getDouble("RMSDeviation")
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val crjson = JSONObject()
|
||||
try {
|
||||
crjson.put("dia", dia)
|
||||
crjson.put("meanDeviation", meanDeviation.toInt())
|
||||
crjson.put("SMRDeviation", smrDeviation)
|
||||
crjson.put("RMSDeviation", rmsDeviation.toInt())
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
return crjson
|
||||
}
|
||||
|
||||
fun equals(obj: DiaDatum): Boolean {
|
||||
var isEqual = true
|
||||
if (dia != obj.dia) isEqual = false
|
||||
if (meanDeviation != obj.meanDeviation) isEqual = false
|
||||
if (smrDeviation != obj.smrDeviation) isEqual = false
|
||||
if (rmsDeviation != obj.rmsDeviation) isEqual = false
|
||||
return isEqual
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.data
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class PeakDatum {
|
||||
|
||||
var peak = 0.0
|
||||
var meanDeviation = 0.0
|
||||
var smrDeviation = 0.0
|
||||
var rmsDeviation = 0.0
|
||||
|
||||
constructor() {}
|
||||
constructor(json: JSONObject) {
|
||||
try {
|
||||
if (json.has("peak")) peak = json.getDouble("peak")
|
||||
if (json.has("meanDeviation")) meanDeviation = json.getDouble("meanDeviation")
|
||||
if (json.has("SMRDeviation")) smrDeviation = json.getDouble("SMRDeviation")
|
||||
if (json.has("RMSDeviation")) rmsDeviation = json.getDouble("RMSDeviation")
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val crjson = JSONObject()
|
||||
try {
|
||||
crjson.put("peak", peak)
|
||||
crjson.put("meanDeviation", meanDeviation.toInt())
|
||||
crjson.put("SMRDeviation", smrDeviation)
|
||||
crjson.put("RMSDeviation", rmsDeviation.toInt())
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
return crjson
|
||||
}
|
||||
|
||||
fun equals(obj: PeakDatum): Boolean {
|
||||
var isEqual = true
|
||||
if (peak != obj.peak) isEqual = false
|
||||
if (meanDeviation != obj.meanDeviation) isEqual = false
|
||||
if (smrDeviation != obj.smrDeviation) isEqual = false
|
||||
if (rmsDeviation != obj.rmsDeviation) isEqual = false
|
||||
return isEqual
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.data
|
||||
|
||||
import info.nightscout.androidaps.utils.DateUtil
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class PreppedGlucose {
|
||||
|
||||
var crData: List<CRDatum>? = ArrayList()
|
||||
var csfGlucoseData: List<BGDatum>? = ArrayList()
|
||||
var isfGlucoseData: List<BGDatum>? = ArrayList()
|
||||
var basalGlucoseData: List<BGDatum>? = ArrayList()
|
||||
var diaDeviations: List<DiaDatum> = ArrayList()
|
||||
var peakDeviations: List<PeakDatum> = ArrayList()
|
||||
var from: Long = 0
|
||||
lateinit var dateUtil: DateUtil
|
||||
|
||||
// to generate same king of json string than oref0-autotune-prep
|
||||
override fun toString(): String {
|
||||
return toString(0)
|
||||
}
|
||||
|
||||
constructor(from: Long, crData: List<CRDatum>?, csfGlucoseData: List<BGDatum>?, isfGlucoseData: List<BGDatum>?, basalGlucoseData: List<BGDatum>?, dateUtil: DateUtil) {
|
||||
this.from = from
|
||||
this.crData = crData
|
||||
this.csfGlucoseData = csfGlucoseData
|
||||
this.isfGlucoseData = isfGlucoseData
|
||||
this.basalGlucoseData = basalGlucoseData
|
||||
this.dateUtil = dateUtil
|
||||
}
|
||||
|
||||
constructor(json: JSONObject?, dateUtil: DateUtil) {
|
||||
if (json == null) return
|
||||
this.dateUtil = dateUtil
|
||||
crData = null
|
||||
csfGlucoseData = null
|
||||
isfGlucoseData = null
|
||||
basalGlucoseData = null
|
||||
try {
|
||||
crData = JsonCRDataToList(json.getJSONArray("CRData"))
|
||||
csfGlucoseData = JsonGlucoseDataToList(json.getJSONArray("CSFGlucoseData"))
|
||||
isfGlucoseData = JsonGlucoseDataToList(json.getJSONArray("ISFGlucoseData"))
|
||||
basalGlucoseData = JsonGlucoseDataToList(json.getJSONArray("basalGlucoseData"))
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonGlucoseDataToList(array: JSONArray): List<BGDatum> {
|
||||
val bgData: MutableList<BGDatum> = ArrayList()
|
||||
for (index in 0 until array.length()) {
|
||||
try {
|
||||
val o = array.getJSONObject(index)
|
||||
bgData.add(BGDatum(o, dateUtil))
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
return bgData
|
||||
}
|
||||
|
||||
private fun JsonCRDataToList(array: JSONArray): List<CRDatum> {
|
||||
val crData: MutableList<CRDatum> = ArrayList()
|
||||
for (index in 0 until array.length()) {
|
||||
try {
|
||||
val o = array.getJSONObject(index)
|
||||
crData.add(CRDatum(o, dateUtil))
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
return crData
|
||||
}
|
||||
|
||||
fun toString(indent: Int): String {
|
||||
var jsonString = ""
|
||||
val json = JSONObject()
|
||||
try {
|
||||
val crjson = JSONArray()
|
||||
for (crd in crData!!) {
|
||||
crjson.put(crd.toJSON())
|
||||
}
|
||||
val csfjson = JSONArray()
|
||||
for (bgd in csfGlucoseData!!) {
|
||||
csfjson.put(bgd.toJSON(true))
|
||||
}
|
||||
val isfjson = JSONArray()
|
||||
for (bgd in isfGlucoseData!!) {
|
||||
isfjson.put(bgd.toJSON(false))
|
||||
}
|
||||
val basaljson = JSONArray()
|
||||
for (bgd in basalGlucoseData!!) {
|
||||
basaljson.put(bgd.toJSON(false))
|
||||
}
|
||||
val diajson = JSONArray()
|
||||
val peakjson = JSONArray()
|
||||
if (diaDeviations.size > 0 || peakDeviations.size > 0) {
|
||||
for (diad in diaDeviations) {
|
||||
diajson.put(diad.toJSON())
|
||||
}
|
||||
for (peakd in peakDeviations) {
|
||||
peakjson.put(peakd.toJSON())
|
||||
}
|
||||
}
|
||||
json.put("CRData", crjson)
|
||||
json.put("CSFGlucoseData", csfjson)
|
||||
json.put("ISFGlucoseData", isfjson)
|
||||
json.put("basalGlucoseData", basaljson)
|
||||
if (diaDeviations.size > 0 || peakDeviations.size > 0) {
|
||||
json.put("diaDeviations", diajson)
|
||||
json.put("peakDeviations", peakjson)
|
||||
}
|
||||
jsonString = if (indent != 0) json.toString(indent) else json.toString()
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
return jsonString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package info.nightscout.androidaps.plugins.general.autotune.events
|
||||
|
||||
import info.nightscout.androidaps.events.Event
|
||||
|
||||
class EventAutotuneUpdateGui : Event()
|
343
app/src/main/res/layout/autotune_fragment.xml
Normal file
343
app/src/main/res/layout/autotune_fragment.xml
Normal file
|
@ -0,0 +1,343 @@
|
|||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".plugins.general.autotune.AutotuneFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/Widget.MaterialComponents.CardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:contentPadding="2dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|center_vertical"
|
||||
android:layout_weight="2"
|
||||
android:gravity="end"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/autotune_profile"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/autotune_select_profile"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/profileList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="5dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|center_vertical"
|
||||
android:layout_weight="2"
|
||||
android:gravity="end"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/autotune_tune_days"
|
||||
android:textSize="14sp" />
|
||||
|
||||
|
||||
<info.nightscout.androidaps.utils.ui.NumberPicker
|
||||
android:id="@+id/tune_days"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginBottom="2dp"
|
||||
app:customContentDescription="@string/careportal_newnstreatment_duration_label" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/Widget.MaterialComponents.CardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:contentPadding="2dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:gravity="end"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/autotune_last_run"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tune_lastrun"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:gravity="end"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/autotune_warning"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tune_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/Widget.MaterialComponents.CardView"
|
||||
android:id="@+id/autotune_results_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:contentPadding="2dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
android:layout_gravity="center">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/autotune_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.gridlayout.widget.GridLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="5dip"
|
||||
app:columnCount="2">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_profileswitch"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/activate_profile"
|
||||
app:icon="@drawable/ic_local_activate"
|
||||
app:iconPadding="-6dp"
|
||||
app:iconTint="@color/ic_local_activate"
|
||||
app:layout_column="0"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="0" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_compare"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/autotune_compare_profile"
|
||||
app:icon="@drawable/ic_compare_profiles"
|
||||
app:iconPadding="-6dp"
|
||||
app:iconTintMode="multiply"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="0" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_copylocal"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/autotune_copy_localprofile_button"
|
||||
app:icon="@drawable/ic_clone_48"
|
||||
app:iconPadding="-6dp"
|
||||
app:iconTintMode="multiply"
|
||||
app:layout_column="0"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_update_profile"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/autotune_update_input_profile_button"
|
||||
app:icon="@drawable/ic_local_save"
|
||||
app:iconPadding="-8dp"
|
||||
app:iconTint="@color/ic_local_save"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_revert_profile"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/autotune_revert_input_profile_button"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_local_reset"
|
||||
app:iconPadding="-6dp"
|
||||
app:iconTint="@color/ic_local_reset"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="1" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_run"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/autotune_run"
|
||||
app:icon="@drawable/ic_autotune"
|
||||
app:iconPadding="-4dp"
|
||||
app:iconTint="@color/ic_local_save"
|
||||
app:layout_column="0"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="2" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/autotune_check_input_profile"
|
||||
style="@style/GrayButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:text="@string/autotune_check_input_profile_button"
|
||||
app:icon="@drawable/ic_home_profile"
|
||||
app:iconPadding="-6dp"
|
||||
app:iconTint="?attr/profileColor"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill"
|
||||
app:layout_row="2" />
|
||||
|
||||
</androidx.gridlayout.widget.GridLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
40
app/src/main/res/xml/pref_autotune.xml
Normal file
40
app/src/main/res/xml/pref_autotune.xml
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<PreferenceCategory
|
||||
android:key="@string/key_autotune_plugin"
|
||||
android:title="@string/autotune_settings"
|
||||
app:initialExpandedChildrenCount="0">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/key_autotune_auto"
|
||||
android:summary="@string/autotune_auto_summary"
|
||||
android:title="@string/autotune_auto_title" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/key_autotune_categorize_uam_as_basal"
|
||||
android:summary="@string/autotune_categorize_uam_as_basal_summary"
|
||||
android:title="@string/autotune_categorize_uam_as_basal_title" />
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="5"
|
||||
android:inputType="number"
|
||||
android:key="@string/key_autotune_default_tune_days"
|
||||
android:summary="@string/autotune_default_tune_days_summary"
|
||||
android:title="@string/autotune_default_tune_days_title" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/key_autotune_circadian_ic_isf"
|
||||
android:summary="@string/autotune_circadian_ic_isf_summary"
|
||||
android:title="@string/autotune_circadian_ic_isf_title" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/key_autotune_additional_log"
|
||||
android:summary="@string/autotune_additional_log_summary"
|
||||
android:title="@string/autotune_additional_log_title" />
|
||||
</PreferenceCategory>
|
||||
</androidx.preference.PreferenceScreen>
|
|
@ -0,0 +1,9 @@
|
|||
package info.nightscout.androidaps.interfaces
|
||||
|
||||
interface Autotune {
|
||||
|
||||
fun aapsAutotune(daysBack: Int, autoSwitch: Boolean, profileToTune: String = ""): String
|
||||
fun atLog(message: String)
|
||||
|
||||
var lastRunSuccess: Boolean
|
||||
}
|
|
@ -559,7 +559,7 @@
|
|||
<string name="autotune_circadian_ic_isf_summary">Autotune will not tune circadian variations, this option only apply the average tuning of IC and ISF to your circadian input profile</string>
|
||||
<string name="autotune_additional_log_title">Include more log information for debugging</string>
|
||||
<string name="autotune_additional_log_summary">Switch on only if requested by dev to send more log information to help debugging Autotune plugin</string>
|
||||
<string name="autotune_default_tune_days_summary">Default number of days of data to be processed by Autotune (up to xx)</string>
|
||||
<string name="autotune_default_tune_days_summary">Default number of days of data to be processed by Autotune (up to 30)</string>
|
||||
<string name="autotune_tunedprofile_name">Tuned</string>
|
||||
<string name="autotune_profile">Profile :</string>
|
||||
<string name="autotune_tune_days">Tune days :</string>
|
||||
|
@ -568,7 +568,7 @@
|
|||
<string name="autotune_select_profile">Select profile to tune</string>
|
||||
<string name="autotune_ic_warning">Autotune works with only one IC value, your profile has %1$d values. Average value is %2$.2fg/U</string>
|
||||
<string name="autotune_isf_warning">Autotune works with only one ISF value, your profile has %1$d values. Average value is %2$.1f%3$s/U</string>
|
||||
<string name="autotune_error">Error in input data, try to reduce the number of days</string>
|
||||
<string name="autotune_error">Error in input data, try to run again autotune or reduce the number of days</string>
|
||||
<string name="autotune_warning_during_run">Autotune calculation started, please be patient</string>
|
||||
<string name="autotune_warning_after_run">Check the results carefully before using it!</string>
|
||||
<string name="autotune_partial_result">Partial result day %1$d / %2$d tuned</string>
|
||||
|
|
Loading…
Reference in a new issue