From d8762bec0ffe0cfb859bac60a28762b13ce222a2 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 10:41:43 +0200 Subject: [PATCH 01/18] Add Autotune in UserEntry --- .../src/main/java/info/nightscout/androidaps/utils/Translator.kt | 1 + .../nightscout/androidaps/utils/userEntry/UserEntryMapper.kt | 1 + .../androidaps/utils/userEntry/UserEntryPresentationHelper.kt | 1 + .../info/nightscout/androidaps/database/entities/UserEntry.kt | 1 + 4 files changed, 4 insertions(+) diff --git a/core/src/main/java/info/nightscout/androidaps/utils/Translator.kt b/core/src/main/java/info/nightscout/androidaps/utils/Translator.kt index c65eea32a1..195d69874c 100644 --- a/core/src/main/java/info/nightscout/androidaps/utils/Translator.kt +++ b/core/src/main/java/info/nightscout/androidaps/utils/Translator.kt @@ -285,6 +285,7 @@ class Translator @Inject internal constructor( Sources.Aaps -> TODO() */ Sources.Automation -> rh.gs(R.string.automation) + Sources.Autotune -> rh.gs(R.string.autotune) Sources.Loop -> rh.gs(R.string.loop) Sources.NSClient -> rh.gs(R.string.ns) Sources.Pump -> rh.gs(R.string.pump) diff --git a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt index 9f2333fbd9..9641ddfa6b 100644 --- a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt +++ b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryMapper.kt @@ -108,6 +108,7 @@ class UserEntryMapper { Announcement (UserEntry.Sources.Announcement), Actions (UserEntry.Sources.Actions), Automation (UserEntry.Sources.Automation), + Autotune (UserEntry.Sources.Autotune), BG (UserEntry.Sources.BG), Aidex (UserEntry.Sources.Aidex), Dexcom (UserEntry.Sources.Dexcom), diff --git a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt index 7ec9e11440..0bc1c3d8ca 100644 --- a/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt +++ b/core/src/main/java/info/nightscout/androidaps/utils/userEntry/UserEntryPresentationHelper.kt @@ -62,6 +62,7 @@ class UserEntryPresentationHelper @Inject constructor( Sources.Announcement -> R.drawable.ic_cp_announcement Sources.Actions -> R.drawable.ic_action Sources.Automation -> R.drawable.ic_automation + Sources.Autotune -> R.drawable.ic_autotune Sources.BG -> R.drawable.ic_generic_cgm Sources.Aidex -> R.drawable.ic_blooddrop_48 Sources.Dexcom -> R.drawable.ic_dexcom_g6 diff --git a/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt b/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt index f6c3d39234..23c659c5c2 100644 --- a/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt +++ b/database/src/main/java/info/nightscout/androidaps/database/entities/UserEntry.kt @@ -138,6 +138,7 @@ data class UserEntry( Announcement, Actions, //From Actions plugin Automation, //From Automation plugin + Autotune, //From Autotune plugin BG, //From BG plugin => Add One Source per BG Source for Calibration or Sensor Change Aidex, Dexcom, From 50d60e1302267a480baac477003730c0a088b2b9 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 13:23:10 +0200 Subject: [PATCH 02/18] MaintenancePlugin add cleaning of old dedicated autotune log file --- .../general/maintenance/MaintenancePlugin.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt index cdbbb6f038..90f051f2a1 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt @@ -69,11 +69,24 @@ class MaintenancePlugin @Inject constructor( val files = logDir.listFiles { _: File?, name: String -> (name.startsWith("AndroidAPS") && name.endsWith(".zip")) } + val autotunefiles = logDir.listFiles { _: File?, name: String -> + (name.startsWith("autotune") && name.endsWith(".zip")) + } + val amount = sp.getInt(R.string.key_logshipper_amount, keep) + val keepIndex = amount - 1 + if (autotunefiles != null && autotunefiles.isNotEmpty()) { + Arrays.sort(autotunefiles) { f1: File, f2: File -> f2.name.compareTo(f1.name) } + var delAutotuneFiles = listOf(*autotunefiles) + if (keepIndex < delAutotuneFiles.size) { + delAutotuneFiles = delAutotuneFiles.subList(keepIndex, delAutotuneFiles.size) + for (file in delAutotuneFiles) { + file.delete() + } + } + } if (files == null || files.isEmpty()) return Arrays.sort(files) { f1: File, f2: File -> f2.name.compareTo(f1.name) } var delFiles = listOf(*files) - val amount = sp.getInt(R.string.key_logshipper_amount, keep) - val keepIndex = amount - 1 if (keepIndex < delFiles.size) { delFiles = delFiles.subList(keepIndex, delFiles.size) for (file in delFiles) { From a708c45bfb48fc26ad27ebfa16edf4a70c8e40d1 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 21:56:28 +0200 Subject: [PATCH 03/18] Add LocalInsulin class and update BolusExtension with iobCalc done with LocalInsulin --- .../androidaps/data/LocalInsulin.kt | 45 +++++++++++++++++++ .../androidaps/extensions/BolusExtension.kt | 7 +++ 2 files changed, 52 insertions(+) create mode 100644 core/src/main/java/info/nightscout/androidaps/data/LocalInsulin.kt diff --git a/core/src/main/java/info/nightscout/androidaps/data/LocalInsulin.kt b/core/src/main/java/info/nightscout/androidaps/data/LocalInsulin.kt new file mode 100644 index 0000000000..c739b34b43 --- /dev/null +++ b/core/src/main/java/info/nightscout/androidaps/data/LocalInsulin.kt @@ -0,0 +1,45 @@ +package info.nightscout.androidaps.data + +import info.nightscout.androidaps.database.entities.Bolus +import kotlin.math.exp +import kotlin.math.pow + +class LocalInsulin constructor(val name:String?, val peak:Int = DEFAULT_PEAK, private val userDefinedDia: Double = DEFAULT_DIA) { + val dia + get(): Double { + val dia = userDefinedDia + return if (dia >= MIN_DIA) { + dia + } else { + MIN_DIA + } + } + + val duration + get() = (60 * 60 * 1000L * dia).toLong() + + fun iobCalcForTreatment(bolus: Bolus, time: Long): Iob { + val result = Iob() + if (bolus.amount != 0.0) { + val bolusTime = bolus.timestamp + val t = (time - bolusTime) / 1000.0 / 60.0 + val td = dia * 60 //getDIA() always >= MIN_DIA + val tp = peak.toDouble() + // force the IOB to 0 if over DIA hours have passed + if (t < td) { + val tau = tp * (1 - tp / td) / (1 - 2 * tp / td) + val a = 2 * tau / td + val S = 1 / (1 - a + (1 + a) * exp(-td / tau)) + result.activityContrib = bolus.amount * (S / tau.pow(2.0)) * t * (1 - t / td) * exp(-t / tau) + result.iobContrib = bolus.amount * (1 - S * (1 - a) * ((t.pow(2.0) / (tau * td * (1 - a)) - t / tau - 1) * Math.exp(-t / tau) + 1)) + } + } + return result + } + + companion object { + private const val MIN_DIA = 5.0 + private const val DEFAULT_DIA = 6.0 + private const val DEFAULT_PEAK = 75 + } +} \ No newline at end of file diff --git a/core/src/main/java/info/nightscout/androidaps/extensions/BolusExtension.kt b/core/src/main/java/info/nightscout/androidaps/extensions/BolusExtension.kt index c89a1c67f2..6c2df8d032 100644 --- a/core/src/main/java/info/nightscout/androidaps/extensions/BolusExtension.kt +++ b/core/src/main/java/info/nightscout/androidaps/extensions/BolusExtension.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.extensions import info.nightscout.androidaps.data.Iob +import info.nightscout.androidaps.data.LocalInsulin import info.nightscout.androidaps.database.embedments.InterfaceIDs import info.nightscout.androidaps.database.entities.Bolus import info.nightscout.androidaps.database.entities.TherapyEvent @@ -16,6 +17,12 @@ fun Bolus.iobCalc(activePlugin: ActivePlugin, time: Long, dia: Double): Iob { return insulinInterface.iobCalcForTreatment(this, time, dia) } +// Add specific calculation for Autotune (reference localInsulin for Peak/dia) +fun Bolus.iobCalc(time: Long, localInsulin: LocalInsulin): Iob { + if (!isValid || type == Bolus.Type.PRIMING ) return Iob() + return localInsulin.iobCalcForTreatment(this, time) +} + fun Bolus.toJson(isAdd: Boolean, dateUtil: DateUtil): JSONObject = JSONObject() .put("eventType", if (type == Bolus.Type.SMB) TherapyEvent.Type.CORRECTION_BOLUS.text else TherapyEvent.Type.MEAL_BOLUS.text) From b1165caef3e7dfc95810e602a9e3c97f239dc57b Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 21:58:32 +0200 Subject: [PATCH 04/18] Add milliSecFromMidnight in Profile interface and Autotune LTag --- .../java/info/nightscout/androidaps/interfaces/Profile.kt | 4 ++++ shared/src/main/java/info/nightscout/shared/logging/LTag.kt | 1 + 2 files changed, 5 insertions(+) diff --git a/core/src/main/java/info/nightscout/androidaps/interfaces/Profile.kt b/core/src/main/java/info/nightscout/androidaps/interfaces/Profile.kt index d935495eb8..c62af2c988 100644 --- a/core/src/main/java/info/nightscout/androidaps/interfaces/Profile.kt +++ b/core/src/main/java/info/nightscout/androidaps/interfaces/Profile.kt @@ -148,6 +148,10 @@ interface Profile { return (passed / 1000).toInt() } + fun milliSecFromMidnight(date: Long): Long { + val passed = DateTime(date).millisOfDay.toLong() + return passed + } /* * Units conversion */ diff --git a/shared/src/main/java/info/nightscout/shared/logging/LTag.kt b/shared/src/main/java/info/nightscout/shared/logging/LTag.kt index df6aafa82b..d9ef1e3c56 100644 --- a/shared/src/main/java/info/nightscout/shared/logging/LTag.kt +++ b/shared/src/main/java/info/nightscout/shared/logging/LTag.kt @@ -5,6 +5,7 @@ enum class LTag(val tag: String, val defaultValue : Boolean = true, val requires APS("APS"), AUTOSENS("AUTOSENS", defaultValue = false), AUTOMATION("AUTOMATION"), + AUTOTUNE("AUTOTUNE", defaultValue = false), BGSOURCE("BGSOURCE"), CONFIGBUILDER("CONFIGBUILDER"), CONSTRAINTS("CONSTRAINTS"), From 3d02c7f57cbe02ecd97d3d4e982cd0a17db4b8a8 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 22:37:20 +0200 Subject: [PATCH 05/18] Add Autotune Plugin --- .../activities/MyPreferenceFragment.kt | 3 + .../nightscout/androidaps/di/AppComponent.kt | 2 + .../nightscout/androidaps/di/AppModule.kt | 2 + .../androidaps/di/AutotuneModule.kt | 23 + .../androidaps/di/FragmentsModule.kt | 2 + .../nightscout/androidaps/di/PluginsModule.kt | 7 + .../plugins/general/autotune/AutotuneCore.kt | 514 ++++++++++++++++ .../plugins/general/autotune/AutotuneFS.kt | 204 +++++++ .../general/autotune/AutotuneFragment.kt | 490 +++++++++++++++ .../plugins/general/autotune/AutotuneIob.kt | 386 ++++++++++++ .../general/autotune/AutotunePlugin.kt | 301 ++++++++++ .../plugins/general/autotune/AutotunePrep.kt | 558 ++++++++++++++++++ .../general/autotune/data/ATProfile.kt | 250 ++++++++ .../plugins/general/autotune/data/BGDatum.kt | 80 +++ .../plugins/general/autotune/data/CRDatum.kt | 54 ++ .../plugins/general/autotune/data/DiaDatum.kt | 44 ++ .../general/autotune/data/PeakDatum.kt | 44 ++ .../general/autotune/data/PreppedGlucose.kt | 117 ++++ .../autotune/events/EventAutotuneUpdateGui.kt | 5 + app/src/main/res/layout/autotune_fragment.xml | 343 +++++++++++ app/src/main/res/xml/pref_autotune.xml | 40 ++ .../androidaps/interfaces/Autotune.kt | 9 + core/src/main/res/values/strings.xml | 4 +- 23 files changed, 3480 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/di/AutotuneModule.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFS.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneIob.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/ATProfile.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/BGDatum.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/CRDatum.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/events/EventAutotuneUpdateGui.kt create mode 100644 app/src/main/res/layout/autotune_fragment.xml create mode 100644 app/src/main/res/xml/pref_autotune.xml create mode 100644 core/src/main/java/info/nightscout/androidaps/interfaces/Autotune.kt diff --git a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt index 19fdfea986..ccd6b6f484 100644 --- a/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/activities/MyPreferenceFragment.kt @@ -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) diff --git a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt index 876aa88311..32b65aca7b 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/AppComponent.kt @@ -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, diff --git a/app/src/main/java/info/nightscout/androidaps/di/AppModule.kt b/app/src/main/java/info/nightscout/androidaps/di/AppModule.kt index 4b3d0797e7..5cb0e30721 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/AppModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/AppModule.kt @@ -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 diff --git a/app/src/main/java/info/nightscout/androidaps/di/AutotuneModule.kt b/app/src/main/java/info/nightscout/androidaps/di/AutotuneModule.kt new file mode 100644 index 0000000000..eaed4a09db --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/di/AutotuneModule.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/di/FragmentsModule.kt b/app/src/main/java/info/nightscout/androidaps/di/FragmentsModule.kt index 9d77adcd26..6ee3126082 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/FragmentsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/FragmentsModule.kt @@ -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 diff --git a/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt b/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt index 33259f2d18..64c2fb24dd 100644 --- a/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/di/PluginsModule.kt @@ -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 diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt new file mode 100644 index 0000000000..ccdc0ab886 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt @@ -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 diaDeviations = preppedGlucose.diaDeviations; + //List 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 = ArrayList() + val bGIs: MutableList = ArrayList() + val avgDeltas: MutableList = ArrayList() + val ratios: MutableList = 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") + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFS.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFS.kt new file mode 100644 index 0000000000..5f1d3a9959 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFS.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt new file mode 100644 index 0000000000..8aa85e33ca --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt @@ -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 = 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") + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneIob.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneIob.kt new file mode 100644 index 0000000000..5dea75afd2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneIob.kt @@ -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() + private var dia: Double = Constants.defaultDIA + var boluses: MutableList = ArrayList() + var meals = ArrayList() + lateinit var glucose: List // newest at index 0 + private lateinit var tempBasals: MutableList + 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() + 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 { + val result: MutableList = 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 { + val result: MutableList = 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt new file mode 100644 index 0000000000..e389477dbe --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt @@ -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 = 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt new file mode 100644 index 0000000000..7f68dc162d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt @@ -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 = autotuneIob.meals + var boluses: MutableList = 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 = 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{ 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 = ArrayList() + var isfGlucoseData: MutableList = ArrayList() + var basalGlucoseData: MutableList = ArrayList() + val uamGlucoseData: MutableList = ArrayList() + val crData: MutableList = 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 = 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{ 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 = 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{ 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 = 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): 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") + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/ATProfile.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/ATProfile.kt new file mode 100644 index 0000000000..b3d78b79db --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/ATProfile.kt @@ -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, 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?): 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/BGDatum.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/BGDatum.kt new file mode 100644 index 0000000000..42c7c88deb --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/BGDatum.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/CRDatum.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/CRDatum.kt new file mode 100644 index 0000000000..14796081b4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/CRDatum.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt new file mode 100644 index 0000000000..255d4ce4d8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt new file mode 100644 index 0000000000..3d6d1e83fc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt new file mode 100644 index 0000000000..cc54df1fac --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt @@ -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? = ArrayList() + var csfGlucoseData: List? = ArrayList() + var isfGlucoseData: List? = ArrayList() + var basalGlucoseData: List? = ArrayList() + var diaDeviations: List = ArrayList() + var peakDeviations: List = 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?, csfGlucoseData: List?, isfGlucoseData: List?, basalGlucoseData: List?, 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 { + val bgData: MutableList = 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 { + val crData: MutableList = 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/events/EventAutotuneUpdateGui.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/events/EventAutotuneUpdateGui.kt new file mode 100644 index 0000000000..2a607d475e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/events/EventAutotuneUpdateGui.kt @@ -0,0 +1,5 @@ +package info.nightscout.androidaps.plugins.general.autotune.events + +import info.nightscout.androidaps.events.Event + +class EventAutotuneUpdateGui : Event() \ No newline at end of file diff --git a/app/src/main/res/layout/autotune_fragment.xml b/app/src/main/res/layout/autotune_fragment.xml new file mode 100644 index 0000000000..8d366acb97 --- /dev/null +++ b/app/src/main/res/layout/autotune_fragment.xml @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/pref_autotune.xml b/app/src/main/res/xml/pref_autotune.xml new file mode 100644 index 0000000000..eff85c18fc --- /dev/null +++ b/app/src/main/res/xml/pref_autotune.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/java/info/nightscout/androidaps/interfaces/Autotune.kt b/core/src/main/java/info/nightscout/androidaps/interfaces/Autotune.kt new file mode 100644 index 0000000000..b90934475e --- /dev/null +++ b/core/src/main/java/info/nightscout/androidaps/interfaces/Autotune.kt @@ -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 +} \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index fc94c704ef..9e7c537e62 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -559,7 +559,7 @@ Autotune will not tune circadian variations, this option only apply the average tuning of IC and ISF to your circadian input profile Include more log information for debugging Switch on only if requested by dev to send more log information to help debugging Autotune plugin - Default number of days of data to be processed by Autotune (up to xx) + Default number of days of data to be processed by Autotune (up to 30) Tuned Profile : Tune days : @@ -568,7 +568,7 @@ Select profile to tune Autotune works with only one IC value, your profile has %1$d values. Average value is %2$.2fg/U Autotune works with only one ISF value, your profile has %1$d values. Average value is %2$.1f%3$s/U - Error in input data, try to reduce the number of days + Error in input data, try to run again autotune or reduce the number of days Autotune calculation started, please be patient Check the results carefully before using it! Partial result day %1$d / %2$d tuned From cfd3ffc6edf6f62e798817ad960452a715eb7c30 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 23:23:38 +0200 Subject: [PATCH 06/18] Add dev and engineering mode --- .../general/autotune/AutotuneFragment.kt | 20 ++++++++++++------- core/src/main/res/values/strings.xml | 2 ++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt index 8aa85e33ca..5cbce91b0a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt @@ -36,6 +36,7 @@ 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.androidaps.utils.buildHelper.BuildHelper import info.nightscout.shared.SafeParse import info.nightscout.shared.sharedPreferences.SP import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -59,6 +60,7 @@ class AutotuneFragment : DaggerFragment() { @Inject lateinit var uel: UserEntryLogger @Inject lateinit var rh: ResourceHelper @Inject lateinit var rxBus: RxBus + @Inject lateinit var buildHelper: BuildHelper @Inject lateinit var injector: HasAndroidInjector private var disposable: CompositeDisposable = CompositeDisposable() @@ -301,16 +303,20 @@ class AutotuneFragment : DaggerFragment() { 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 + if (buildHelper.isEngineeringMode()) { + 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.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run) + } else + binding.tuneWarning.text = rh.gs(R.string.autotune_engineering_mode_warning) binding.autotuneCompare.visibility = View.VISIBLE - binding.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run) - } else { + } else if (buildHelper.isDev()) { binding.autotuneRun.visibility = View.VISIBLE binding.autotuneCheckInputProfile.visibility = View.VISIBLE - } + } else + binding.tuneWarning.text = rh.gs(R.string.autotune_dev_warning) binding.tuneLastrun.text = dateUtil.dateAndTimeString(autotunePlugin.lastRun) showResults() } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 9e7c537e62..eed67a2158 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -566,6 +566,8 @@ Last run : Warning : Select profile to tune + You can currently only run Autotune on dev builds of AAPS + Copy or Activate Tuned profile is only available in engineering mode Autotune works with only one IC value, your profile has %1$d values. Average value is %2$.2fg/U Autotune works with only one ISF value, your profile has %1$d values. Average value is %2$.1f%3$s/U Error in input data, try to run again autotune or reduce the number of days From 9cec7282341e3f676ef6402e0dcfaa0f3810b405 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 5 May 2022 23:27:07 +0200 Subject: [PATCH 07/18] Automation Day selection (duration) and Active profile selection for Autotune --- .../plugins/general/automation/elements/InputDuration.kt | 5 ++++- .../plugins/general/automation/elements/InputProfileName.kt | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.kt index 6e275190e9..05dc3ef41b 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.kt @@ -13,7 +13,7 @@ class InputDuration( ) : Element() { enum class TimeUnit { - MINUTES, HOURS + MINUTES, HOURS, DAYS } override fun addToLayout(root: LinearLayout) { @@ -21,6 +21,9 @@ class InputDuration( if (unit == TimeUnit.MINUTES) { numberPicker = MinutesNumberPicker(root.context, null) numberPicker.setParams(value.toDouble(), 5.0, 24 * 60.0, 10.0, DecimalFormat("0"), false, root.findViewById(R.id.ok)) + } else if (unit == TimeUnit.DAYS) { + numberPicker = MinutesNumberPicker(root.context, null) + numberPicker.setParams(value.toDouble(), 1.0, 30.0, 1.0, DecimalFormat("0"), false, root.findViewById(R.id.ok)) } else { numberPicker = NumberPicker(root.context, null) numberPicker.setParams(value.toDouble(), 1.0, 24.0, 1.0, DecimalFormat("0"), false, root.findViewById(R.id.ok)) diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputProfileName.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputProfileName.kt index 14f6bf46c7..9cdac1b01e 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputProfileName.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputProfileName.kt @@ -10,14 +10,15 @@ import info.nightscout.androidaps.automation.R import info.nightscout.androidaps.interfaces.ActivePlugin import info.nightscout.androidaps.interfaces.ResourceHelper -class InputProfileName(private val rh: ResourceHelper, private val activePlugin: ActivePlugin, val name: String = "") : Element() { +class InputProfileName(private val rh: ResourceHelper, private val activePlugin: ActivePlugin, val name: String = "", val addActive: Boolean = false) : Element() { var value: String = name override fun addToLayout(root: LinearLayout) { val profileStore = activePlugin.activeProfileSource.profile ?: return val profileList = profileStore.getProfileList() - + if (addActive) + profileList.add(0, rh.gs(R.string.active)) root.addView( Spinner(root.context).apply { adapter = ArrayAdapter(root.context, R.layout.spinner_centered, profileList).apply { From ee1d3cab4a3ecec3a6baf4d1887c4b27f3b59771 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 6 May 2022 00:08:46 +0200 Subject: [PATCH 08/18] Add ActionRunAutotune in Automation --- .../automation/di/AutomationModule.kt | 1 + .../general/automation/AutomationPlugin.kt | 1 + .../general/automation/actions/Action.kt | 2 + .../automation/actions/ActionRunAutotune.kt | 91 +++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt diff --git a/automation/src/main/java/info/nightscout/androidaps/automation/di/AutomationModule.kt b/automation/src/main/java/info/nightscout/androidaps/automation/di/AutomationModule.kt index b5bf1ccaf1..0d40a894fa 100644 --- a/automation/src/main/java/info/nightscout/androidaps/automation/di/AutomationModule.kt +++ b/automation/src/main/java/info/nightscout/androidaps/automation/di/AutomationModule.kt @@ -42,6 +42,7 @@ abstract class AutomationModule { @ContributesAndroidInjector abstract fun actionCarePortalEventInjector(): ActionCarePortalEvent @ContributesAndroidInjector abstract fun actionProfileSwitchInjector(): ActionProfileSwitch @ContributesAndroidInjector abstract fun actionProfileSwitchPercentInjector(): ActionProfileSwitchPercent + @ContributesAndroidInjector abstract fun actionRunAutotuneInjector(): ActionRunAutotune @ContributesAndroidInjector abstract fun actionSendSMSInjector(): ActionSendSMS @ContributesAndroidInjector abstract fun actionStartTempTargetInjector(): ActionStartTempTarget @ContributesAndroidInjector abstract fun actionStopTempTargetInjector(): ActionStopTempTarget diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt index 3335aad5c5..f9dbd66fda 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt @@ -334,6 +334,7 @@ class AutomationPlugin @Inject constructor( ActionCarePortalEvent(injector), ActionProfileSwitchPercent(injector), ActionProfileSwitch(injector), + ActionRunAutotune(injector), ActionSendSMS(injector) ) } diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.kt index a12ba56a7e..5503d84d0c 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.kt @@ -71,6 +71,8 @@ abstract class Action(val injector: HasAndroidInjector) { ActionProfileSwitch::class.java.simpleName -> ActionProfileSwitch(injector).fromJSON(data.toString()) ActionProfileSwitchPercent::class.java.name, ActionProfileSwitchPercent::class.java.simpleName -> ActionProfileSwitchPercent(injector).fromJSON(data.toString()) + ActionRunAutotune::class.java.name, + ActionRunAutotune::class.java.simpleName -> ActionRunAutotune(injector).fromJSON(data.toString()) ActionSendSMS::class.java.name, ActionSendSMS::class.java.simpleName -> ActionSendSMS(injector).fromJSON(data.toString()) ActionStartTempTarget::class.java.name, diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt new file mode 100644 index 0000000000..f68767189a --- /dev/null +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt @@ -0,0 +1,91 @@ +package info.nightscout.androidaps.plugins.general.automation.actions + +import android.widget.LinearLayout +import androidx.annotation.DrawableRes +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.automation.R +import info.nightscout.androidaps.data.PumpEnactResult +import info.nightscout.androidaps.interfaces.ActivePlugin +import info.nightscout.androidaps.interfaces.Autotune +import info.nightscout.androidaps.interfaces.ProfileFunction +import info.nightscout.androidaps.interfaces.ResourceHelper +import info.nightscout.androidaps.logging.UserEntryLogger +import info.nightscout.androidaps.plugins.general.automation.elements.InputDuration +import info.nightscout.androidaps.plugins.general.automation.elements.InputProfileName +import info.nightscout.androidaps.plugins.general.automation.elements.LabelWithElement +import info.nightscout.androidaps.plugins.general.automation.elements.LayoutBuilder +import info.nightscout.androidaps.queue.Callback +import info.nightscout.androidaps.utils.JsonHelper +import info.nightscout.androidaps.utils.buildHelper.BuildHelper +import info.nightscout.shared.logging.LTag +import info.nightscout.shared.sharedPreferences.SP +import org.json.JSONObject +import javax.inject.Inject + +class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var autotunePlugin: Autotune + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var activePlugin: ActivePlugin + @Inject lateinit var sp: SP + @Inject lateinit var uel: UserEntryLogger + @Inject lateinit var buildHelper: BuildHelper + + var defaultValue = 0 + private var inputProfileName = InputProfileName(rh, activePlugin, "", true) + private var daysBack = InputDuration(0, InputDuration.TimeUnit.DAYS) + + override fun friendlyName(): Int = R.string.autotune_run + override fun shortDescription(): String = resourceHelper.gs(R.string.autotune_profile_name, inputProfileName.value) + @DrawableRes override fun icon(): Int = R.drawable.ic_actions_profileswitch + + override fun doAction(callback: Callback) { + val autoSwitch = sp.getBoolean(R.string.key_autotune_auto, false) + val profileName = if (inputProfileName.value == rh.gs(R.string.active)) "" else inputProfileName.value + var message = if (autoSwitch) R.string.autotune_run_with_autoswitch else R.string.autotune_run_without_autoswitch + Thread { + autotunePlugin.atLog("[Automation] Run Autotune $profileName, ${daysBack.value} days, Autoswitch $autoSwitch") + autotunePlugin.aapsAutotune(daysBack.value, autoSwitch, profileName) + if (!autotunePlugin.lastRunSuccess) { + message = R.string.autotune_run_with_error + aapsLogger.error(LTag.AUTOMATION, "Error during Autotune Run") + } + callback.result(PumpEnactResult(injector).success(autotunePlugin.lastRunSuccess).comment(message))?.run() + }.start() + return + } + + override fun generateDialog(root: LinearLayout) { + if (defaultValue == 0) + defaultValue = sp.getInt(R.string.key_autotune_default_tune_days, 5) + daysBack.value = defaultValue + LayoutBuilder() + .add(LabelWithElement(rh, rh.gs(R.string.autotune_select_profile), "", inputProfileName)) + .add(LabelWithElement(rh, rh.gs(R.string.autotune_tune_days), "", daysBack)) + .build(root) + } + + override fun hasDialog(): Boolean = true + + override fun toJSON(): String { + val data = JSONObject() + .put("profileToTune", inputProfileName.value) + .put("tunedays", daysBack.value) + return JSONObject() + .put("type", this.javaClass.name) + .put("data", data) + .toString() + } + + override fun fromJSON(data: String): Action { + val o = JSONObject(data) + inputProfileName.value = JsonHelper.safeGetString(o, "profileToTune", "") + defaultValue = JsonHelper.safeGetInt(o, "tunedays") + if (defaultValue == 0) + defaultValue = sp.getInt(R.string.key_autotune_default_tune_days, 5) + daysBack.value = defaultValue + return this + } + + override fun isValid(): Boolean = profileFunction.getProfile() != null +} \ No newline at end of file From 6465995b264e7386bb48b94ccf4d524f9406b94b Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 6 May 2022 00:10:10 +0200 Subject: [PATCH 09/18] Add dev and engineeering mode in Automation --- .../general/automation/actions/ActionRunAutotune.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt index f68767189a..0b2ddf4354 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt @@ -40,18 +40,21 @@ class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { @DrawableRes override fun icon(): Int = R.drawable.ic_actions_profileswitch override fun doAction(callback: Callback) { - val autoSwitch = sp.getBoolean(R.string.key_autotune_auto, false) + val autoSwitch = sp.getBoolean(R.string.key_autotune_auto, false) && buildHelper.isEngineeringMode() val profileName = if (inputProfileName.value == rh.gs(R.string.active)) "" else inputProfileName.value var message = if (autoSwitch) R.string.autotune_run_with_autoswitch else R.string.autotune_run_without_autoswitch Thread { + if (buildHelper.isDev()) { autotunePlugin.atLog("[Automation] Run Autotune $profileName, ${daysBack.value} days, Autoswitch $autoSwitch") autotunePlugin.aapsAutotune(daysBack.value, autoSwitch, profileName) if (!autotunePlugin.lastRunSuccess) { message = R.string.autotune_run_with_error aapsLogger.error(LTag.AUTOMATION, "Error during Autotune Run") } - callback.result(PumpEnactResult(injector).success(autotunePlugin.lastRunSuccess).comment(message))?.run() - }.start() + } else + message = R.string.autotune_dev_warning + callback.result(PumpEnactResult(injector).success(autotunePlugin.lastRunSuccess).comment(message))?.run() + }.start() return } From 344a896d51d7c5b16c7a092d8aab70a1ddbd8307 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 6 May 2022 00:11:47 +0200 Subject: [PATCH 10/18] Clean warning in AutotuneFragment --- .../general/autotune/AutotuneFragment.kt | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt index 5cbce91b0a..83ef53e354 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt @@ -43,7 +43,7 @@ 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 org.slf4j.LoggerFactory import java.text.DecimalFormat import java.util.* import javax.inject.Inject @@ -64,7 +64,7 @@ class AutotuneFragment : DaggerFragment() { @Inject lateinit var injector: HasAndroidInjector private var disposable: CompositeDisposable = CompositeDisposable() - private val log = LoggerFactory.getLogger(AutotunePlugin::class.java) + //private val log = LoggerFactory.getLogger(AutotunePlugin::class.java) private var _binding: AutotuneFragmentBinding? = null private lateinit var profileStore: ProfileStore private var profileName = "" @@ -81,7 +81,7 @@ class AutotuneFragment : DaggerFragment() { 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()) + if (autotunePlugin.lastNbDays.isEmpty()) 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) @@ -98,9 +98,9 @@ class AutotuneFragment : DaggerFragment() { autotunePlugin.calculationRunning = true autotunePlugin.lastNbDays = daysBack.toString() log("Run Autotune $profileName, $daysBack days") - Thread(Runnable { + Thread { autotunePlugin.aapsAutotune(daysBack, false, profileName) - }).start() + }.start() updateGui() } binding.profileList.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, _ -> @@ -300,23 +300,27 @@ class AutotuneFragment : DaggerFragment() { 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) { - if (buildHelper.isEngineeringMode()) { - 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.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run) - } else - binding.tuneWarning.text = rh.gs(R.string.autotune_engineering_mode_warning) - binding.autotuneCompare.visibility = View.VISIBLE - } else if (buildHelper.isDev()) { - binding.autotuneRun.visibility = View.VISIBLE - binding.autotuneCheckInputProfile.visibility = View.VISIBLE - } else - binding.tuneWarning.text = rh.gs(R.string.autotune_dev_warning) + when { + autotunePlugin.calculationRunning -> { + binding.tuneWarning.text = rh.gs(R.string.autotune_warning_during_run) + } + autotunePlugin.lastRunSuccess -> { + if (buildHelper.isEngineeringMode()) { + 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.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run) + } else + binding.tuneWarning.text = rh.gs(R.string.autotune_engineering_mode_warning) + binding.autotuneCompare.visibility = View.VISIBLE + } + buildHelper.isDev() -> { + binding.autotuneRun.visibility = View.VISIBLE + binding.autotuneCheckInputProfile.visibility = View.VISIBLE + } + else -> binding.tuneWarning.text = rh.gs(R.string.autotune_dev_warning) + } binding.tuneLastrun.text = dateUtil.dateAndTimeString(autotunePlugin.lastRun) showResults() } @@ -326,7 +330,7 @@ class AutotuneFragment : DaggerFragment() { 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 + } else if (!runToday || autotunePlugin.result.isEmpty()) { //if new day reinit result, default days, warning and button's visibility resetParam(!runToday) } } @@ -366,7 +370,7 @@ class AutotuneFragment : DaggerFragment() { 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()) { + if (binding.tuneDays.text.isNotEmpty()) { try { if (autotunePlugin.calculationRunning) binding.tuneDays.value = autotunePlugin.lastNbDays.toDouble() @@ -427,7 +431,7 @@ class AutotuneFragment : DaggerFragment() { }.start() } - fun toTableRowHeader(basal:Boolean = false): TableRow = + private 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 } @@ -458,7 +462,7 @@ class AutotuneFragment : DaggerFragment() { }) } - fun toTableRowValue(hour: String, inputValue: Double, tunedValue: Double, missing: String = ""): TableRow = + private 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 } From 4b2fda3278981d20fab8d8c098f226599036026c Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 6 May 2022 11:58:39 +0200 Subject: [PATCH 11/18] Enabled only in dev and Engineering mode --- .../plugins/general/autotune/AutotuneFragment.kt | 8 +------- .../plugins/general/autotune/AutotunePlugin.kt | 4 ++++ .../general/automation/AutomationPlugin.kt | 2 +- .../automation/actions/ActionRunAutotune.kt | 15 ++++++--------- core/src/main/res/values/strings.xml | 2 -- 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt index 83ef53e354..94b7ddb865 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt @@ -36,7 +36,6 @@ 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.androidaps.utils.buildHelper.BuildHelper import info.nightscout.shared.SafeParse import info.nightscout.shared.sharedPreferences.SP import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -60,7 +59,6 @@ class AutotuneFragment : DaggerFragment() { @Inject lateinit var uel: UserEntryLogger @Inject lateinit var rh: ResourceHelper @Inject lateinit var rxBus: RxBus - @Inject lateinit var buildHelper: BuildHelper @Inject lateinit var injector: HasAndroidInjector private var disposable: CompositeDisposable = CompositeDisposable() @@ -305,21 +303,17 @@ class AutotuneFragment : DaggerFragment() { binding.tuneWarning.text = rh.gs(R.string.autotune_warning_during_run) } autotunePlugin.lastRunSuccess -> { - if (buildHelper.isEngineeringMode()) { 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.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run) - } else - binding.tuneWarning.text = rh.gs(R.string.autotune_engineering_mode_warning) binding.autotuneCompare.visibility = View.VISIBLE } - buildHelper.isDev() -> { + else -> { binding.autotuneRun.visibility = View.VISIBLE binding.autotuneCheckInputProfile.visibility = View.VISIBLE } - else -> binding.tuneWarning.text = rh.gs(R.string.autotune_dev_warning) } binding.tuneLastrun.text = dateUtil.dateAndTimeString(autotunePlugin.lastRun) showResults() diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt index e389477dbe..546ba6bbb8 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt @@ -18,6 +18,7 @@ import info.nightscout.androidaps.plugins.profile.local.events.EventLocalProfile import info.nightscout.androidaps.utils.DateUtil import info.nightscout.androidaps.utils.MidnightTime import info.nightscout.androidaps.utils.T +import info.nightscout.androidaps.utils.buildHelper.BuildHelper import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.sharedPreferences.SP import org.json.JSONException @@ -48,6 +49,7 @@ class AutotunePlugin @Inject constructor( private val autotuneIob: AutotuneIob, private val autotunePrep: AutotunePrep, private val autotuneCore: AutotuneCore, + private val buildHelper:BuildHelper, private val uel: UserEntryLogger, aapsLogger: AAPSLogger ) : PluginBase(PluginDescription() @@ -295,6 +297,8 @@ class AutotunePlugin @Inject constructor( atLog("[Plugin] $message") } + override fun specialEnableCondition(): Boolean = buildHelper.isEngineeringMode() && buildHelper.isDev() + override fun atLog(message: String) { autotuneFS.atLog(message) } diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt index 22abb1b54e..137ddff7f5 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt @@ -332,7 +332,7 @@ class AutomationPlugin @Inject constructor( ActionCarePortalEvent(injector), ActionProfileSwitchPercent(injector), ActionProfileSwitch(injector), - ActionRunAutotune(injector), + //ActionRunAutotune(injector), ActionSendSMS(injector) ) } diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt index 0b2ddf4354..1c7c3f7b28 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt @@ -44,15 +44,12 @@ class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { val profileName = if (inputProfileName.value == rh.gs(R.string.active)) "" else inputProfileName.value var message = if (autoSwitch) R.string.autotune_run_with_autoswitch else R.string.autotune_run_without_autoswitch Thread { - if (buildHelper.isDev()) { - autotunePlugin.atLog("[Automation] Run Autotune $profileName, ${daysBack.value} days, Autoswitch $autoSwitch") - autotunePlugin.aapsAutotune(daysBack.value, autoSwitch, profileName) - if (!autotunePlugin.lastRunSuccess) { - message = R.string.autotune_run_with_error - aapsLogger.error(LTag.AUTOMATION, "Error during Autotune Run") - } - } else - message = R.string.autotune_dev_warning + autotunePlugin.atLog("[Automation] Run Autotune $profileName, ${daysBack.value} days, Autoswitch $autoSwitch") + autotunePlugin.aapsAutotune(daysBack.value, autoSwitch, profileName) + if (!autotunePlugin.lastRunSuccess) { + message = R.string.autotune_run_with_error + aapsLogger.error(LTag.AUTOMATION, "Error during Autotune Run") + } callback.result(PumpEnactResult(injector).success(autotunePlugin.lastRunSuccess).comment(message))?.run() }.start() return diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index eed67a2158..9e7c537e62 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -566,8 +566,6 @@ Last run : Warning : Select profile to tune - You can currently only run Autotune on dev builds of AAPS - Copy or Activate Tuned profile is only available in engineering mode Autotune works with only one IC value, your profile has %1$d values. Average value is %2$.2fg/U Autotune works with only one ISF value, your profile has %1$d values. Average value is %2$.1f%3$s/U Error in input data, try to run again autotune or reduce the number of days From 70ed332b7cc8fdc9ec2eb19ba813748b2647f10f Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 6 May 2022 12:10:23 +0200 Subject: [PATCH 12/18] Remove Engineering constraint in RunAutomation automation --- .../plugins/general/automation/actions/ActionRunAutotune.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt index 1c7c3f7b28..d756ab7423 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt @@ -16,7 +16,6 @@ import info.nightscout.androidaps.plugins.general.automation.elements.LabelWithE import info.nightscout.androidaps.plugins.general.automation.elements.LayoutBuilder import info.nightscout.androidaps.queue.Callback import info.nightscout.androidaps.utils.JsonHelper -import info.nightscout.androidaps.utils.buildHelper.BuildHelper import info.nightscout.shared.logging.LTag import info.nightscout.shared.sharedPreferences.SP import org.json.JSONObject @@ -29,7 +28,6 @@ class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { @Inject lateinit var activePlugin: ActivePlugin @Inject lateinit var sp: SP @Inject lateinit var uel: UserEntryLogger - @Inject lateinit var buildHelper: BuildHelper var defaultValue = 0 private var inputProfileName = InputProfileName(rh, activePlugin, "", true) @@ -40,7 +38,7 @@ class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { @DrawableRes override fun icon(): Int = R.drawable.ic_actions_profileswitch override fun doAction(callback: Callback) { - val autoSwitch = sp.getBoolean(R.string.key_autotune_auto, false) && buildHelper.isEngineeringMode() + val autoSwitch = sp.getBoolean(R.string.key_autotune_auto, false) val profileName = if (inputProfileName.value == rh.gs(R.string.active)) "" else inputProfileName.value var message = if (autoSwitch) R.string.autotune_run_with_autoswitch else R.string.autotune_run_without_autoswitch Thread { From bf46d8a6050617f38d2795eabf7a479de3686291 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 6 May 2022 23:52:26 +0200 Subject: [PATCH 13/18] Check if AutotunePlugin is enabled in ActionRunAutotune --- .../androidaps/plugins/general/automation/AutomationPlugin.kt | 2 +- .../plugins/general/automation/actions/ActionRunAutotune.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt index 137ddff7f5..22abb1b54e 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt @@ -332,7 +332,7 @@ class AutomationPlugin @Inject constructor( ActionCarePortalEvent(injector), ActionProfileSwitchPercent(injector), ActionProfileSwitch(injector), - //ActionRunAutotune(injector), + ActionRunAutotune(injector), ActionSendSMS(injector) ) } diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt index d756ab7423..2fc8026157 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionRunAutotune.kt @@ -85,5 +85,5 @@ class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { return this } - override fun isValid(): Boolean = profileFunction.getProfile() != null + override fun isValid(): Boolean = profileFunction.getProfile() != null && activePlugin.getSpecificPluginsListByInterface(Autotune::class.java).first().isEnabled() } \ No newline at end of file From e7da675e1e6df697d8bf14fd4ec4151a47ad3671 Mon Sep 17 00:00:00 2001 From: Philoul Date: Mon, 9 May 2022 08:59:57 +0200 Subject: [PATCH 14/18] Add Tune insulin Curve option --- .../plugins/general/autotune/AutotuneCore.kt | 208 +++++------ .../general/autotune/AutotuneFragment.kt | 18 +- .../general/autotune/AutotunePlugin.kt | 16 +- .../plugins/general/autotune/AutotunePrep.kt | 346 ++++++++---------- .../data/{DiaDatum.kt => DiaDeviation.kt} | 19 +- .../data/{PeakDatum.kt => PeakDeviation.kt} | 21 +- .../general/autotune/data/PreppedGlucose.kt | 30 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_autotune.xml | 8 +- core/src/main/res/values/strings.xml | 8 +- 10 files changed, 323 insertions(+), 352 deletions(-) rename app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/{DiaDatum.kt => DiaDeviation.kt} (63%) rename app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/{PeakDatum.kt => PeakDeviation.kt} (59%) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt index ccdc0ab886..9bfb851952 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneCore.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.plugins.general.autotune import info.nightscout.androidaps.R +import info.nightscout.androidaps.data.LocalInsulin 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 @@ -28,16 +29,14 @@ class AutotuneCore @Inject constructor( 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) + var dia = previousAutotune.dia + var peak = previousAutotune.peak val csfGlucose = preppedGlucose.csfGlucoseData val isfGlucose = preppedGlucose.isfGlucoseData val basalGlucose = preppedGlucose.basalGlucoseData val crData = preppedGlucose.crData - //List diaDeviations = preppedGlucose.diaDeviations; - //List peakDeviations = preppedGlucose.peakDeviations; + val diaDeviations = preppedGlucose.diaDeviations + val peakDeviations = preppedGlucose.peakDeviations val pumpISF = pumpProfile.isf val pumpCarbRatio = pumpProfile.ic val pumpCSF = pumpISF / pumpCarbRatio @@ -46,92 +45,98 @@ class AutotuneCore @Inject constructor( 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); - * } - * } - * - */ + // tune DIA + var newDia = dia + if (diaDeviations.size > 0) + { + val currentDiaMeanDev = diaDeviations[2].meanDeviation + val currentDiaRMSDev = diaDeviations[2].rmsDeviation + //Console.WriteLine(DIA,currentDIAMeanDev,currentDIARMSDev); + var minMeanDeviations = 1000000.0 + var minRmsDeviations = 1000000.0 + var meanBest = 2 + var rmsBest = 2 + for (i in 0..diaDeviations.size-1) + { + val meanDeviations = diaDeviations[i].meanDeviation + val rmsDeviations = diaDeviations[i].rmsDeviation + if (meanDeviations < minMeanDeviations) + { + minMeanDeviations = Round.roundTo(meanDeviations, 0.001) + meanBest = i + } + if (rmsDeviations < minRmsDeviations) + { + minRmsDeviations = Round.roundTo(rmsDeviations, 0.001) + rmsBest = i + } + } + log("Best insulinEndTime for meanDeviations: ${diaDeviations[meanBest].dia} hours") + log("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.0) + { + log("insulinEndTime maximum is 12h: not raising further") + newDia = 12.0 + } + if (newDia != dia) + log("Adjusting insulinEndTime from $dia to $newDia hours") + else + log("Leaving insulinEndTime unchanged at $dia hours") + } + + // tune insulinPeakTime + var newPeak = peak + if (peakDeviations.size > 2) + { + val currentPeakMeanDev = peakDeviations[2].meanDeviation + val currentPeakRMSDev = peakDeviations[2].rmsDeviation + //Console.WriteLine(currentPeakMeanDev); + var minMeanDeviations = 1000000.0 + var minRmsDeviations = 1000000.0 + var meanBest = 2 + var rmsBest = 2 + for (i in 0..peakDeviations.size-1) + { + val meanDeviations = peakDeviations[i].meanDeviation; + val rmsDeviations = peakDeviations[i].rmsDeviation; + if (meanDeviations < minMeanDeviations) + { + minMeanDeviations = Round.roundTo(meanDeviations, 0.001) + meanBest = i + } + if (rmsDeviations < minRmsDeviations) + { + minRmsDeviations = Round.roundTo(rmsDeviations, 0.001) + rmsBest = i + } + } + log("Best insulinPeakTime for meanDeviations: ${peakDeviations[meanBest].peak} minutes") + log("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) + log("Adjusting insulinPeakTime from " + peak + " to " + newPeak + " minutes") + else + log("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 @@ -142,7 +147,7 @@ class AutotuneCore @Inject constructor( //autotune-core (lib/autotune/index.js) #149-#165 var crTotalCarbs = 0.0 var crTotalInsulin = 0.0 - for (i in crData!!.indices) { + for (i in crData.indices) { val crDatum = crData[i] val crBGChange = crDatum.crEndBG - crDatum.crInitialBG val crInsulinReq = crBGChange / isf @@ -181,7 +186,7 @@ class AutotuneCore @Inject constructor( // look at net deviations for each hour for (hour in 0..23) { var deviations = 0.0 - for (i in basalGlucose!!.indices) { + for (i in basalGlucose.indices) { val BGTime = Calendar.getInstance() //var BGTime: Date? = null if (basalGlucose[i].date != 0L) { @@ -300,7 +305,7 @@ class AutotuneCore @Inject constructor( //log.debug(CSFGlucose[0].mealAbsorption); //log.debug(CSFGlucose[0]); //autotune-core (lib/autotune/index.js) #346-#365 - for (i in csfGlucose!!.indices) { + for (i in csfGlucose.indices) { //log.debug(CSFGlucose[i].mealAbsorption, i); if (csfGlucose[i].mealAbsorption === "start") { deviations = 0.0 @@ -412,7 +417,7 @@ class AutotuneCore @Inject constructor( val avgDeltas: MutableList = ArrayList() val ratios: MutableList = ArrayList() var count = 0 - for (i in isfGlucose!!.indices) { + for (i in isfGlucose.indices) { val deviation = isfGlucose[i].deviation isfDeviations.add(deviation) val BGI = isfGlucose[i].bgi @@ -497,13 +502,10 @@ class AutotuneCore @Inject constructor( 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.dia = newDia + previousAutotune.peak = newPeak + val localInsulin = LocalInsulin("Ins_$newPeak-$newDia", newPeak, newDia) + previousAutotune.localInsulin = localInsulin previousAutotune.updateProfile() return previousAutotune } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt index 94b7ddb865..912c6a64ed 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotuneFragment.kt @@ -384,6 +384,7 @@ class AutotuneFragment : DaggerFragment() { if (autotunePlugin.result.isNotBlank()) { var toMgDl = 1.0 if (profileFunction.getUnits() == GlucoseUnit.MMOL) toMgDl = Constants.MMOLL_TO_MGDL + var isf_Format = if (profileFunction.getUnits() == GlucoseUnit.MMOL) "%.2f" else "%.1f" binding.autotuneResults.addView( TableLayout(context).also { layout -> layout.addView( @@ -395,8 +396,13 @@ class AutotuneFragment : DaggerFragment() { }) 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))) + val tuneInsulin = sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false) + if (tuneInsulin) { + layout.addView(toTableRowValue(rh.gs(R.string.insulin_peak), autotunePlugin.pumpProfile.localInsulin.peak.toDouble(), tuned.localInsulin.peak.toDouble(), "%.0f")) + layout.addView(toTableRowValue(rh.gs(R.string.dia), Round.roundTo(autotunePlugin.pumpProfile.localInsulin.dia, 0.1), Round.roundTo(tuned.localInsulin.dia, 0.1),"%.1f")) + } + layout.addView(toTableRowValue(rh.gs(R.string.isf_short), Round.roundTo(autotunePlugin.pumpProfile.isf / toMgDl, 0.001), Round.roundTo(tuned.isf / toMgDl, 0.001), isf_Format)) + layout.addView(toTableRowValue(rh.gs(R.string.ic_short), Round.roundTo(autotunePlugin.pumpProfile.ic, 0.001), Round.roundTo(tuned.ic, 0.001), "%.2f")) layout.addView( TextView(context).apply { text = rh.gs(R.string.basal) @@ -413,7 +419,7 @@ class AutotuneFragment : DaggerFragment() { 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(time, autotunePlugin.pumpProfile.basal[h], tuned.basal[h], "%.3f", tuned.basalUntuned[h].toString())) } layout.addView(toTableRowValue("∑", totalPump, totalTuned, " ")) } @@ -456,7 +462,7 @@ class AutotuneFragment : DaggerFragment() { }) } - private fun toTableRowValue(hour: String, inputValue: Double, tunedValue: Double, missing: String = ""): TableRow = + private fun toTableRowValue(hour: String, inputValue: Double, tunedValue: Double, format:String = "%.3f", 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 } @@ -469,12 +475,12 @@ class AutotuneFragment : DaggerFragment() { row.addView(TextView(context).apply { layoutParams = lp.apply { column = 1 } textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text = String.format("%.3f", inputValue) + text = String.format(format, inputValue) }) row.addView(TextView(context).apply { layoutParams = lp.apply { column = 2 } textAlignment = TextView.TEXT_ALIGNMENT_CENTER - text = String.format("%.3f", tunedValue) + text = String.format(format, tunedValue) }) row.addView(TextView(context).apply { layoutParams = lp.apply { column = 3 } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt index 546ba6bbb8..9a7839f600 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePlugin.kt @@ -31,7 +31,6 @@ 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) */ @@ -94,7 +93,7 @@ class AutotunePlugin @Inject constructor( 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 + val 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 @@ -120,7 +119,7 @@ class AutotunePlugin @Inject constructor( 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 + preppedGlucose = autotunePrep.categorize(tunedProfile) //<=> autotune.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine } if (preppedGlucose == null || tunedProfile == null) { @@ -204,9 +203,14 @@ class AutotunePlugin @Inject constructor( var strResult = line strResult += rh.gs(R.string.autotune_log_title) strResult += line + val tuneInsulin = sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false) + if (tuneInsulin) { + strResult += rh.gs(R.string.autotune_log_peak, rh.gs(R.string.insulin_peak), pumpProfile.localInsulin.peak, tunedProfile.localInsulin.peak) + strResult += rh.gs(R.string.autotune_log_dia, rh.gs(R.string.ic_short), pumpProfile.localInsulin.dia, tunedProfile.localInsulin.dia) + } // 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 += rh.gs(R.string.autotune_log_ic_isf, rh.gs(R.string.isf_short), pumpProfile.isf, tunedProfile.isf) + strResult += rh.gs(R.string.autotune_log_ic_isf, rh.gs(R.string.ic_short), pumpProfile.ic, tunedProfile.ic) strResult += line var totalBasal = 0.0 var totalTuned = 0.0 @@ -232,7 +236,7 @@ class AutotunePlugin @Inject constructor( 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 = "" + val optInsulinCurve = if (sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false)) "-i=true" else "" try { jsonSettings.put("datestring", dateUtil.toISOString(runDate)) jsonSettings.put("dateutc", dateUtil.toISOAsUTC(runDate)) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt index 7f68dc162d..9e0b5d704f 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/AutotunePrep.kt @@ -3,14 +3,13 @@ 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.plugins.general.autotune.data.* 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.MidnightTime import info.nightscout.androidaps.utils.Round +import info.nightscout.androidaps.utils.T import info.nightscout.shared.sharedPreferences.SP import java.util.* import javax.inject.Inject @@ -23,11 +22,121 @@ class AutotunePrep @Inject constructor( private val autotuneFS: AutotuneFS, private val autotuneIob: AutotuneIob ) { + fun categorize(tunedprofile: ATProfile): PreppedGlucose? { + val preppedGlucose = categorizeBGDatums(tunedprofile, tunedprofile.localInsulin) + val tuneInsulin = sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false) + if (tuneInsulin) { + var minDeviations = 1000000.0 + val diaDeviations: MutableList = ArrayList() + val peakDeviations: MutableList = ArrayList() + val currentDIA = tunedprofile.localInsulin.dia + val currentPeak = tunedprofile.localInsulin.peak + + var dia = currentDIA - 2 + val endDIA = currentDIA + 2 + while (dia <= endDIA) + { + var sqrtDeviations = 0.0 + var deviations = 0.0 + var deviationsSq = 0.0 + val localInsulin = LocalInsulin("Ins_$currentPeak-$dia", currentPeak, dia) + val curve_output = categorizeBGDatums(tunedprofile, localInsulin, false) + val basalGlucose = curve_output?.basalGlucoseData + + basalGlucose?.let { + for (hour in 0..23) { + for (i in 0..(basalGlucose.size-1)) { + val myHour = ((basalGlucose[i].date - MidnightTime.calc(basalGlucose[i].date)) / T.hours(1).msecs()).toInt() + if (hour == myHour) { + sqrtDeviations += Math.pow(Math.abs(basalGlucose[i].deviation), 0.5) + deviations += Math.abs(basalGlucose[i].deviation) + deviationsSq += Math.pow(basalGlucose[i].deviation, 2.0) + } + } + } + + val meanDeviation = Round.roundTo(Math.abs(deviations / basalGlucose.size), 0.001) + val smrDeviation = Round.roundTo(Math.pow(sqrtDeviations / basalGlucose.size, 2.0), 0.001) + val rmsDeviation = Round.roundTo(Math.pow(deviationsSq / basalGlucose.size, 0.5), 0.001) + log("insulinEndTime $dia meanDeviation: $meanDeviation SMRDeviation: $smrDeviation RMSDeviation: $rmsDeviation (mg/dL)") + diaDeviations.add( + DiaDeviation( + dia = dia, + meanDeviation = meanDeviation, + smrDeviation = smrDeviation, + rmsDeviation = rmsDeviation + ) + ) + } + preppedGlucose?.diaDeviations = diaDeviations + + deviations = Round.roundTo(deviations, 0.001) + if (deviations < minDeviations) + minDeviations = Round.roundTo(deviations, 0.001) + dia += 1.0 + } + + // consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', JSMath.Round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); + //consoleError(diaDeviations); + + minDeviations = 1000000.0 + var peak = currentPeak - 10 + val endPeak = currentPeak + 10 + while (peak <= endPeak) + { + var sqrtDeviations = 0.0 + var deviations = 0.0 + var deviationsSq = 0.0 + val localInsulin = LocalInsulin("Ins_$peak-$currentDIA", peak, currentDIA) + val curve_output = categorizeBGDatums(tunedprofile, localInsulin, false) + val basalGlucose = curve_output?.basalGlucoseData + + basalGlucose?.let { + for (hour in 0..23) { + for (i in 0..(basalGlucose.size - 1)) { + val myHour = ((basalGlucose[i].date - MidnightTime.calc(basalGlucose[i].date)) / T.hours(1).msecs()).toInt() + if (hour == myHour) { + //console.error(basalGlucose[i].deviation); + sqrtDeviations += Math.pow(Math.abs(basalGlucose[i].deviation), 0.5) + deviations += Math.abs(basalGlucose[i].deviation) + deviationsSq += Math.pow(basalGlucose[i].deviation, 2.0) + } + } + } + + val meanDeviation = Round.roundTo(deviations / basalGlucose.size, 0.001) + val smrDeviation = Round.roundTo(Math.pow(sqrtDeviations / basalGlucose.size, 2.0), 0.001) + val rmsDeviation = Round.roundTo(Math.pow(deviationsSq / basalGlucose.size, 0.5), 0.001) + log("insulinPeakTime $peak meanDeviation: $meanDeviation SMRDeviation: $smrDeviation RMSDeviation: $rmsDeviation (mg/dL)") + peakDeviations.add( + PeakDeviation + ( + peak = peak, + meanDeviation = meanDeviation, + smrDeviation = smrDeviation, + rmsDeviation = rmsDeviation, + ) + ) + } + + deviations = Round.roundTo(deviations, 0.001); + if (deviations < minDeviations) + minDeviations = Round.roundTo(deviations, 0.001) + peak += 5 + } + //consoleError($"Optimum insulinPeakTime {newPeak} mean deviation: {JSMath.Round(minDeviations/basalGlucose.Count, 3)} (mg/dL)"); + //consoleError(peakDeviations); + preppedGlucose?.peakDeviations = peakDeviations + } + + return preppedGlucose + } + // private static Logger log = LoggerFactory.getLogger(AutotunePlugin.class); - fun categorizeBGDatums(tunedprofile: ATProfile, localInsulin: LocalInsulin): PreppedGlucose? { + fun categorizeBGDatums(tunedprofile: ATProfile, localInsulin: LocalInsulin, verbose: Boolean = true): PreppedGlucose? { //lib/meals is called before to get only meals data (in AAPS it's done in AutotuneIob) - var treatments: MutableList = autotuneIob.meals - var boluses: MutableList = autotuneIob.boluses + val treatments: MutableList = autotuneIob.meals + val boluses: MutableList = 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 = ArrayList() @@ -37,7 +146,8 @@ class AutotunePrep @Inject constructor( } } if (glucose.size == 0 || glucoseData.size == 0 ) { - log("No BG value received") + if (verbose) + log("No BG value received") return null } @@ -49,11 +159,13 @@ class AutotunePrep @Inject constructor( //val boluses = 0 //val maxCarbs = 0 if (treatments.size == 0) { - log("No Carbs entries") + if (verbose) + log("No Carbs entries") //return null } if (autotuneIob.boluses.size == 0) { - log("No treatment received") + if (verbose) + log("No treatment received") return null } @@ -141,7 +253,8 @@ class AutotunePrep @Inject constructor( } avgDelta = (bg - bucketedData[i + 4].value) / 4 } else { - log("Could not find glucose data") + if (verbose) + log("Could not find glucose data") } avgDelta = Round.roundTo(avgDelta, 0.01) glucoseDatum.avgDelta = avgDelta @@ -207,7 +320,8 @@ class AutotunePrep @Inject constructor( crInitialIOB = iob.iob crInitialBG = glucoseDatum.value crInitialCarbTime = glucoseDatum.date - log("CRInitialIOB: " + crInitialIOB + " CRInitialBG: " + crInitialBG + " CRInitialCarbTime: " + dateUtil.toISOString(crInitialCarbTime)) + if (verbose) + 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) { @@ -219,7 +333,8 @@ class AutotunePrep @Inject constructor( val crEndIOB = iob.iob val crEndBG = glucoseDatum.value val crEndTime = glucoseDatum.date - log("CREndIOB: " + crEndIOB + " CREndBG: " + crEndBG + " CREndTime: " + dateUtil.toISOString(crEndTime)) + if (verbose) + log("CREndIOB: " + crEndIOB + " CREndBG: " + crEndBG + " CREndTime: " + dateUtil.toISOString(crEndTime)) val crDatum = CRDatum(dateUtil) crDatum.crInitialBG = crInitialBG crDatum.crInitialIOB = crInitialIOB @@ -234,7 +349,8 @@ class AutotunePrep @Inject constructor( //log.debug(CREndTime - CRInitialCarbTime, CRElapsedMinutes); if (CRElapsedMinutes < 60 || i == 1 && mealCOB > 0) { - log("Ignoring $CRElapsedMinutes m CR period.") + if (verbose) + log("Ignoring $CRElapsedMinutes m CR period.") } else { crData.add(crDatum) } @@ -262,7 +378,8 @@ class AutotunePrep @Inject constructor( //log.debug(type); if (type != "csf") { glucoseDatum.mealAbsorption = "start" - log(glucoseDatum.mealAbsorption + " carb absorption") + if (verbose) + log(glucoseDatum.mealAbsorption + " carb absorption") } type = "csf" glucoseDatum.mealCarbs = mealCarbs.toInt() @@ -272,7 +389,8 @@ class AutotunePrep @Inject constructor( // 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 (verbose) + log(csfGlucoseData[csfGlucoseData.size - 1].mealAbsorption + " carb absorption") } if (iob.iob > 2 * currentBasal || deviation > 6 || uam) { uam = if (deviation > 0) { @@ -282,13 +400,15 @@ class AutotunePrep @Inject constructor( } if (type != "uam") { glucoseDatum.uamAbsorption = "start" - log(glucoseDatum.uamAbsorption + " unannnounced meal absorption") + if (verbose) + log(glucoseDatum.uamAbsorption + " unannnounced meal absorption") } type = "uam" uamGlucoseData.add(glucoseDatum) } else { if (type == "uam") { - log("end unannounced meal absorption") + if (verbose) + 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 @@ -313,7 +433,8 @@ class AutotunePrep @Inject constructor( } } // 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) + if (verbose) + 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) } //**************************************************************************************************************************************** @@ -328,16 +449,20 @@ class AutotunePrep @Inject constructor( 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.") + if (verbose) + 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.") + if (verbose) + 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") + if (verbose) { + 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% @@ -348,10 +473,12 @@ class AutotunePrep @Inject constructor( } //log.debug(newBasalGlucose); basalGlucoseData = newBasalGlucose - log("and selecting the lowest 50%, leaving " + basalGlucoseData.size + " basal+UAM ones") + if (verbose) + 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") + if (verbose) + 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{ 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 @@ -361,178 +488,29 @@ class AutotunePrep @Inject constructor( } //console.error(newISFGlucose); isfGlucoseData = newISFGlucose - log("and selecting the lowest 50%, leaving " + isfGlucoseData.size + " ISF+UAM ones") + if (verbose) + 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") + if (verbose) { + 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 + if (verbose) + log("CRData: " + crData.size + " CSFGlucoseData: " + csfGlucoseData.size + " ISFGlucoseData: " + isfGlucoseData.size + " BasalGlucoseData: " + basalGlucoseData.size) -/* 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 diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDeviation.kt similarity index 63% rename from app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt rename to app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDeviation.kt index 255d4ce4d8..9be016d39c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDatum.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/DiaDeviation.kt @@ -3,15 +3,9 @@ package info.nightscout.androidaps.plugins.general.autotune.data import org.json.JSONException import org.json.JSONObject -class DiaDatum { +class DiaDeviation(var dia: Double = 0.0, var meanDeviation: Double = 0.0, var smrDeviation: Double = 0.0, var rmsDeviation: Double = 0.0) { - var dia = 0.0 - var meanDeviation = 0.0 - var smrDeviation = 0.0 - var rmsDeviation = 0.0 - - constructor() {} - constructor(json: JSONObject) { + constructor(json: JSONObject) : this() { try { if (json.has("dia")) dia = json.getDouble("dia") if (json.has("meanDeviation")) meanDeviation = json.getDouble("meanDeviation") @@ -32,13 +26,4 @@ class DiaDatum { } 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 - } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDeviation.kt similarity index 59% rename from app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt rename to app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDeviation.kt index 3d6d1e83fc..c9e3f66581 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDatum.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PeakDeviation.kt @@ -3,17 +3,11 @@ package info.nightscout.androidaps.plugins.general.autotune.data import org.json.JSONException import org.json.JSONObject -class PeakDatum { +class PeakDeviation(var peak: Int = 0, var meanDeviation: Double = 0.0, var smrDeviation: Double = 0.0, var rmsDeviation: Double = 0.0) { - var peak = 0.0 - var meanDeviation = 0.0 - var smrDeviation = 0.0 - var rmsDeviation = 0.0 - - constructor() {} - constructor(json: JSONObject) { + constructor(json: JSONObject) : this() { try { - if (json.has("peak")) peak = json.getDouble("peak") + if (json.has("peak")) peak = json.getInt("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") @@ -32,13 +26,4 @@ class PeakDatum { } 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 - } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt index cc54df1fac..3015dd6122 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/autotune/data/PreppedGlucose.kt @@ -8,12 +8,12 @@ import java.util.* class PreppedGlucose { - var crData: List? = ArrayList() - var csfGlucoseData: List? = ArrayList() - var isfGlucoseData: List? = ArrayList() - var basalGlucoseData: List? = ArrayList() - var diaDeviations: List = ArrayList() - var peakDeviations: List = ArrayList() + var crData: List = ArrayList() + var csfGlucoseData: List = ArrayList() + var isfGlucoseData: List = ArrayList() + var basalGlucoseData: List = ArrayList() + var diaDeviations: List = ArrayList() + var peakDeviations: List = ArrayList() var from: Long = 0 lateinit var dateUtil: DateUtil @@ -22,7 +22,7 @@ class PreppedGlucose { return toString(0) } - constructor(from: Long, crData: List?, csfGlucoseData: List?, isfGlucoseData: List?, basalGlucoseData: List?, dateUtil: DateUtil) { + constructor(from: Long, crData: List, csfGlucoseData: List, isfGlucoseData: List, basalGlucoseData: List, dateUtil: DateUtil) { this.from = from this.crData = crData this.csfGlucoseData = csfGlucoseData @@ -34,10 +34,10 @@ class PreppedGlucose { constructor(json: JSONObject?, dateUtil: DateUtil) { if (json == null) return this.dateUtil = dateUtil - crData = null - csfGlucoseData = null - isfGlucoseData = null - basalGlucoseData = null + crData = ArrayList() + csfGlucoseData = ArrayList() + isfGlucoseData = ArrayList() + basalGlucoseData = ArrayList() try { crData = JsonCRDataToList(json.getJSONArray("CRData")) csfGlucoseData = JsonGlucoseDataToList(json.getJSONArray("CSFGlucoseData")) @@ -76,19 +76,19 @@ class PreppedGlucose { val json = JSONObject() try { val crjson = JSONArray() - for (crd in crData!!) { + for (crd in crData) { crjson.put(crd.toJSON()) } val csfjson = JSONArray() - for (bgd in csfGlucoseData!!) { + for (bgd in csfGlucoseData) { csfjson.put(bgd.toJSON(true)) } val isfjson = JSONArray() - for (bgd in isfGlucoseData!!) { + for (bgd in isfGlucoseData) { isfjson.put(bgd.toJSON(false)) } val basaljson = JSONArray() - for (bgd in basalGlucoseData!!) { + for (bgd in basalGlucoseData) { basaljson.put(bgd.toJSON(false)) } val diajson = JSONArray() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2eea647d5..3e155b06c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -533,6 +533,7 @@ insulin_oref_peak IOB Curve Peak Time Peak Time [min] + Peak Free-Peak Oref Rapid-Acting Oref Ultra-Rapid Oref diff --git a/app/src/main/res/xml/pref_autotune.xml b/app/src/main/res/xml/pref_autotune.xml index eff85c18fc..4da9c14ade 100644 --- a/app/src/main/res/xml/pref_autotune.xml +++ b/app/src/main/res/xml/pref_autotune.xml @@ -4,7 +4,7 @@ + app:initialExpandedChildrenCount="10"> + + insulin_oref_peak autotune_auto categorize_uam_as_basal + autotune_tune_insulin_curve autotune_default_tune_days autotune_circadian_ic_isf autotune_additional_log @@ -554,6 +555,8 @@ If enabled, Autotune will automatically update and switch to input profile after calculation from an automation rule. Categorize UAM as basal Enable only if you have reliably entered all carbs eaten, with this option sudden rises seen by Autotune will be used to recommend changes to the basal rate. + Tune insulin curve + Enable only if you use free peak. This option will tune peak and DIA durations Number of days of data Apply average result in circadian IC/ISF Autotune will not tune circadian variations, this option only apply the average tuning of IC and ISF to your circadian input profile @@ -589,8 +592,9 @@ Profile invalid |Param|Profile|Tuned|%/Miss.\n +------------------------------------------\n - | %1$4.4s |\t%2$3.3f |\t%3$3.3f |\n - | %1$4.4s |\t%2$3.3f |\t%3$3.3f |\n + | %1$4.4s |\t%2$d |\t%3$d |\n + | %1$4.4s |\t%2$3.1f |\t%3$3.1f |\n + | %1$4.4s | %2$3.3f |\t%3$3.3f |\n |\t%1$02.0f\t| %2$3.3f |%3$3.3f\t| %5$.0f%% / %4$d\n |\t∑\t|\t%1$3.1f |\t%2$3.1f |\n Autotune runned without profile switch From 58205e794b6b128a48d8f246cb44209f2194b01e Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 9 May 2022 22:51:16 +0200 Subject: [PATCH 15/18] gradle update --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d2b9b12afb..161e2edfb2 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } // jacoco 0.2 } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.2.0' classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index de175d5f9e..3a2a3a704e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl = https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl = https\://services.gradle.org/distributions/gradle-7.3.3-all.zip From 3f954a419054f16ea04900865abf92a026b84729 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 May 2022 08:09:48 +0000 Subject: [PATCH 16/18] Bump dagger_version from 2.41 to 2.42 Bumps `dagger_version` from 2.41 to 2.42. Updates `dagger-compiler` from 2.41 to 2.42 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.41...dagger-2.42) Updates `dagger-android-processor` from 2.41 to 2.42 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.41...dagger-2.42) Updates `dagger` from 2.41 to 2.42 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.41...dagger-2.42) Updates `dagger-android` from 2.41 to 2.42 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.41...dagger-2.42) Updates `dagger-android-support` from 2.41 to 2.42 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.41...dagger-2.42) --- updated-dependencies: - dependency-name: com.google.dagger:dagger-compiler dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.google.dagger:dagger-android-processor dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.google.dagger:dagger dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.google.dagger:dagger-android dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.google.dagger:dagger-android-support dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 161e2edfb2..b83b7308db 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { rxkotlin_version = '3.0.1' room_version = '2.4.2' lifecycle_version = '2.4.1' - dagger_version = '2.41' + dagger_version = '2.42' coroutines_version = '1.6.1' activity_version = '1.3.1' fragmentktx_version = '1.3.6' From f12e93fcf3220f7582a379d059e315923dc808e4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 10 May 2022 11:44:31 +0200 Subject: [PATCH 17/18] New Crowdin updates (#1702) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations exam.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (Danish) * New translations strings.xml (French) * Update source file strings.xml * New translations strings.xml (Czech) * New translations strings.xml (Norwegian) * New translations strings.xml (Norwegian) * New translations strings.xml (Norwegian) * New translations objectives.xml (Portuguese, Brazilian) * New translations strings.xml (Portuguese, Brazilian) * Update source file strings.xml * Update source file strings.xml * New translations strings.xml (Slovak) * New translations strings.xml (Czech) * New translations strings.xml (Slovak) * New translations strings.xml (Spanish) * New translations strings.xml (Czech) * New translations strings.xml (Danish) * New translations strings.xml (Norwegian) * New translations strings.xml (Turkish) * New translations strings.xml (French) --- app/src/main/res/values-cs-rCZ/strings.xml | 1 + app/src/main/res/values-da-rDK/exam.xml | 2 + app/src/main/res/values-da-rDK/strings.xml | 120 ++++++++++++++ app/src/main/res/values-pt-rBR/objectives.xml | 4 +- app/src/main/res/values-pt-rBR/strings.xml | 48 +++--- app/src/main/res/values-sk-rSK/strings.xml | 1 + .../src/main/res/values-cs-rCZ/strings.xml | 3 + .../src/main/res/values-da-rDK/strings.xml | 11 ++ .../src/main/res/values-fr-rFR/strings.xml | 2 + combo/src/main/res/values-da-rDK/strings.xml | 6 + core/src/main/res/values-cs-rCZ/strings.xml | 2 - core/src/main/res/values-da-rDK/strings.xml | 60 +++++++ core/src/main/res/values-es-rES/strings.xml | 2 - core/src/main/res/values-fr-rFR/strings.xml | 2 - core/src/main/res/values-no-rNO/strings.xml | 43 +++++ core/src/main/res/values-sk-rSK/strings.xml | 6 +- core/src/main/res/values-tr-rTR/strings.xml | 2 - dana/src/main/res/values-da-rDK/strings.xml | 115 +++++++++++++ .../src/main/res/values-da-rDK/strings.xml | 151 ++++++++++++++++++ .../src/main/res/values-no-rNO/strings.xml | 2 + .../src/main/res/values-da-rDK/strings.xml | 2 + .../src/main/res/values-da-rDK/strings.xml | 1 + wear/src/main/res/values-da-rDK/strings.xml | 51 ++++++ wear/src/main/res/values-no-rNO/strings.xml | 12 ++ 24 files changed, 613 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 84a3263b48..a40cae4fe9 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -442,6 +442,7 @@ Detekce neoznámených jídel Čas vrcholu IOB křivky Vrchol křivky [min] + Vrchol Volitelný vrchol - Oref Rychle působící - Oref Ultra rychlý - Oref diff --git a/app/src/main/res/values-da-rDK/exam.xml b/app/src/main/res/values-da-rDK/exam.xml index 751e9af3c7..31dc1e91d2 100644 --- a/app/src/main/res/values-da-rDK/exam.xml +++ b/app/src/main/res/values-da-rDK/exam.xml @@ -104,9 +104,11 @@ Hvor kan du søge efter hjælp til AndroidAPS? Du kan bede om råd i \"AndroidAPS-users\" Facebook-gruppen. Du bør læse (og genlæse) AndroidAPS dokumentation. + Du kan bede om råd og logge tekniske problemer eller andre problemer i AndroidAPS Discord. Du bør spørge din diabetessygeplejerske/endokrinolog. https://androidaps.readthedocs.io/en/latest/EN/Installing-AndroidAPS/Update-to-new-version.html#troubleshooting https://www.facebook.com/groups/AndroidAPSUsers/ + https://discord.gg/4fQUWHZ4Mw Insulin Plugins Hvilken insulin skal du bruge sammen med Ultra-Rapid Oref-pluginnet? Fiasp® diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 4b84b79e76..bb5672460a 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -25,6 +25,7 @@ Synkroniserer dine data med NightScout Status for algoritmen i 2017 Seneste algoritme for avancerede brugere + Seneste algoritme for avancerede brugere med dynamisk/automatisk ISF Viser den aktuelle tilstand af dit loop og knapper til de mest almindelige handlinger Viser en løbende notifikation med en kort oversigt over, hvad dit loop gør Definér en profil, der er tilgængelig offline. @@ -160,6 +161,7 @@ Slutbrugerlicensaftale MÅ IKKE BRUGES TIL AT TRÆFFE MEDICINSKE BESLUTNINGER. DER ER INGEN GARANTI FOR PROGRAMMET, I DET OMFANG GÆLDENDE LOV TILLADER DET. UNDTAGEN NÅR DET ELLERS ER ANFØRT SKRIFTLIGT, AT RETTIGHEDSHAVERE OG / ELLER ANDRE PARTER LEVERER PROGRAMMET \"SOM BESET\" UDEN NOGEN FORM FOR GARANTI, HVERKEN UDTRYKT ELLER UNDERFORSTÅET, HERUNDER, MEN IKKE BEGRÆNSET TIL, DE UNDERFORSTÅELIGHEDER, DER ER FORBUNDET MED EGNETHED OG EGNETHED TIL ET BESTEMT FORMÅL. HELE RISIKOEN MED HENSYN TIL KVALITETEN OG YDEEVNEN AF PROGRAMMET ER DIN. HVIS PROGRAMMET VISER SIG AT VÆRE DEFEKT, BÆRER DU SELV OMKOSTNINGERNE VED ALLE NØDVENDIGE SERVICER, REPARATIONER ELLER KORREKTIONER SOM ER NØDVENDIGE. JEG FORSTÅR OG ER ENIG + Gem Genindlæs profil SMS Kommunikator Tilladte telefonnumre @@ -244,6 +246,7 @@ Ur Send alle data igen Åbn indstillinger på ur + Basalrate Basal værdi under minimum. Profil ikke angivet! BG: Sidste BG: @@ -264,6 +267,7 @@ KONF LOOP OAPS + DynISF LP HJEM VPUMPE @@ -429,6 +433,9 @@ Aktiver udsendelse til andre apps (såsom xDrip+). Aktiver ikke hvis du har mere end én version af AAPS eller NSClient installeret! Aktiver lokale udsendelser. OpenAPS SMB + Dynamisk ISF + DynamicISF-justeringsfaktor %% + Justeringsfaktor for Dynamisk ISF. Indstil mere end 100%% for mere aggressive korrektionsdoser og mindre end 100%% for mindre aggressive korrektioner. Aktiver UAM Aktiver SMB Brug Super Mikro Boluser i stedet for midlertidig basal for hurtigere handling @@ -496,6 +503,7 @@ Kun negative COB beregning Midlertidig mål beregning + Beregning i % Loop aktiveret APS valgt NSClient har skrivetilladelse @@ -656,6 +664,10 @@ Følsomhed hæver midlertidige mål Når der påvises følsomhed, skal målglukosen hæves Ren AndroidAPS startet + Vis ugyldige + Skjul ugyldige + Fjern elementer + Sortér elementer Gemte indstillinger fundet Bemærk: Hvis du aktiverer og opretter forbindelse til en hardwarepumpe, vil AndroidAPS kopiere de basale indstillinger fra profilen til pumpen, overskrive den eksisterende basal rate lagret på pumpen. Sørg for, at du har den korrekte basal indstilling i AndroidAPS. Hvis du ikke er sikker på eller ikke ønsker at overskrive de basale indstillinger på din pumpe, tryk på annuller og gentag skift til pumpen på et senere tidspunkt. Behandlingsdata ukomplette @@ -751,6 +763,8 @@ Profilnavn indeholder punktum.\nDette understøttes ikke af NS.\nProfilen er ikke uploadet til NS. Nedre værdi for målområde (kun visning) Øvre værdi for målområde (kun visning) + Alder + Vægt ID: Send Mest almindelige profil: @@ -800,6 +814,7 @@ På hver follower telefon installeres Authenticator app, der understøtter RFC 6238 TOTP tokens. Populære gratis apps er:\n • Authy\n • Google Authenticator\n • LastPass Authenticator\n • FreeOTP Authenticator Ved at nulstille autentificering gør du alle allerede proviserede autentificatorer ugyldige. Du bliver nødt til at opsætte dem igen! Forudsigelser + Behandlinger Afvigelses hældning Godkendelse mislykkedes Absolut insulin @@ -869,6 +884,8 @@ Accepter profil skift indtastet gennem NS eller NSClient Modtag APS offline begivenheder Accepter APS Offline begivenheder indtastet gennem NS eller NSClient + Modtag TBR og EB + Accepter TBR og EB indtastet gennem en anden instans Modtag insulin Accepter insulin via NS eller NSClient (det er ikke afgivet, kun beregnet til IOB) Modtag kulhydrater @@ -891,6 +908,7 @@ Fejl Reducer upload hastighed BG data status + Fjern BG aflæsninger Indstik alder patch pumpe alder Patch pumpe @@ -898,8 +916,110 @@ BG for tæt:\n%1$s\n%2$s Identifikation (e-mail, FB eller Discord alias osv.) Identifikation ikke indstillet i udvikler-tilstand + dialog + nuværende blodglukose + korrekt resultat med % + korrekt resultat med enheder Ikke tilgængelig + høj + inden for området + lav + falder hurtigt + falder + falder langsomt + stabil + stiger langsomt + stiger + stiger hurtigt + ingen + ukendt + graf + blodglukose kvalitet + genberegnet + dobbelt postering + insulin + blodglucose + forældet + Indstil påmindelse + tilføj ny profil + klon nuværende profil + slet nuværende profil + tilføj ny til listen + Vælg mørk, lys eller følg systemtemaet + App Farvetema + Mørkt tema + Lyst tema + Brug enhedens tema + Midlertidigmål ukendt forudindstilling: %1$s + Annullér aktuelt midlertidig mål? + Forskellige enheder brugt på ur og telefon! + 0-mål - annuller midlertidigt mål? + Min-BS udenfor området! + Max-BS udenfor området! + Midlertidigt mål:\nMin: %1$s\nMax: %2$s\nVarighed: %3$s + Midlertigt mål:\nMål: %1$s\nVarighed: %2$s + Midlertigt mål:\Grund: %1$s\nMål: %2$s\nVarighed: %3$s + Hurtigguide: %1$s\nInsulin: %2$.2fE\nKH: %3$dg + Guide:\nInsulin: %1$.2fE\nKH: %2$dg + Vis post på enhed: + Valgt guide er ikke længere tilgængeligt. Opdater venligst din widget + Ingen nylig BG til at basere beregningen på! + Ingen aktiv profil angivet! + Ukendt COB! BG læsning mangler eller nylig app genstart? + KH begrænsninger overtrådt! + Calc (IC: %2$.1f, ISF: %2$.1f) fra:\" + Kulhydrater: %1$.2fE + COB: %1$.0fg %2$.2fE + BS: %1$.2fE + Basal IOB: %1$.2fE + Bolus IOB: %1$.2fE + Superbolus: %1$.2fE + 15\' trend: %1$.2fE + Procent: %1$.2fE x %2$d%% ≈ %3$.2fE + Overtrædelse af insulinbegrænsning!\nKan ikke levere %1$.2fE + Midl: %1$s + %1$s til %2$s + Ingen pumpe tilgængelig! + Ukendt kommando: + Procentdel + Program standard + Vis ugyldige / fjernede poster + Skjul ugyldige / fjernede poster + Vælg profil, du vil redigere + Opdater fra Nightscout + Fjern valgte elementer + Vælg for at fjerne + Profil ændringer + Midlertidig mål + Kulhydrater og bolus + Er du sikker på, at du vil fjerne %1$d elementer? + Ingen poster tilgængelige + Skjul loop + Vis loop + %1$d valgt + Sortér + Dialog annulleret + Under + Inden for området + Over + Vis loop poster + Skjul loop poster + AndroidAPS widget + Indstil gennemsigtighed + Loop status + QR-kode til opsætning af engangs kodeord + Åbn indstillinger + indstil KH alarm + Alle + Telefon + Ur + kun på ur + kun på telefon + træk og slip håndtering + GlucoRx Aidex + Aidex + Modtag BG-værdier fra GlucoRx Aidex CGMS. diff --git a/app/src/main/res/values-pt-rBR/objectives.xml b/app/src/main/res/values-pt-rBR/objectives.xml index 8da349f4d8..b60c9a7dad 100644 --- a/app/src/main/res/values-pt-rBR/objectives.xml +++ b/app/src/main/res/values-pt-rBR/objectives.xml @@ -39,7 +39,7 @@ Código aceito Código inválido Prove seu conhecimento - Estude as perguntas. Haverá quatro respostas possíveis para cada pergunta. Pode ter mais do que uma resposta correta. Por favor, marque todas as que estão corretas e selecione VERIFY. + Estude as perguntas. Haverá quatro respostas possíveis para cada pergunta. Pode ter mais do que uma resposta correta. Por favor, marque todas as que estão corretas e selecione VERIFICAR. Resposta desativada até: %1$s Resposta errada! Próximo inacabado @@ -49,7 +49,7 @@ https://androidaps.readthedocs.io/en/latest/EN/Getting-Started/Screenshots.html#the-homescreen https://androidaps.readthedocs.io/en/latest/EN/Getting-Started/Screenshots.html#config-builder https://androidaps.readthedocs.io/en/latest/EN/Getting-Started/Screenshots.html#the-homescreen - Não está ligado à internet + Não está conectado à internet Falha no tempo de recuperação Requisitos de objetivo não cumpridos diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 61b79dd173..5767a5823b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -285,9 +285,9 @@ Autosens também ajusta os alvos Valor padrão: true\nÉ usado para permitir que autosens possa ajustar os valores alvo de glucose no sangue (BG), além de ISF e basais. Valor padrão: 2\nBolus Snooze (pausa após bolus) é executado depois de realizar um bolus por refeição Desta maneira o algoritmo não irá contrariar com temporárias baixas logo depois da refeição. O valor padrão é 2; Então uma duração de ação da insulina (DIA) de 5h significa que o Bolus Snooze irá ser gradualmente reduzido ao longo de 2,5 horas = 5/2 = DIA/Valor padrão. - Valor padrão: 3.0 para detecção avançada de refeições (AMA) ou 8.0 para super micro bolus (SMB). Esta é a configuração padrão para o calculo de quanto varia a cada 5 min a glucose no sangue (BG) devido à absorção de carboidratos. O padrão é 3mg/ dl / 5min. Isso afeta a rapidez com que decaem as calorias no corpo (COB), e quantos carboidratos terão de ser considerados no cálculo da previsão de BG, quando é que a BG está baixando mais do que o esperado ou não subindo como esperado. - Atenção!\n Normalmente não é necessário modificar os valores abaixo. Por favor PRESSIONE AQUI e LEIA o texto para garantir que ENTENDE as consequenciais antes de alterar algum destes valores. - SMS número de telefone inválido + Valor padrão: 3.0 para assitência avançada de refeições (AAR) ou 8.0 para super micro bolus (SMB). Esta é a configuração padrão para o calculo de quanto varia a cada 5 min a glicemia (BG) devido à absorção de carboidratos. O padrão é 3mg/dl/5min. Isso afeta a rapidez com que decaem os carboidratos ativos (CA) e quanta absorção de carboidrato será considerada no cálculo da previsão de glicemia futura, tornando possível notar que glicemia está baixando mais do que o esperado ou não subindo como esperado. + Atenção!\n Normalmente não é necessário modificar os valores abaixo. Por favor PRESSIONE AQUI e LEIA o texto para garantir que ENTENDE as consequências antes de alterar qualquer um destes valores. + Número de telefone inválido para comunicação por SMS Calibração xDrip+ não está instalado Calibração enviada para o xDrip+ @@ -309,41 +309,41 @@ NS API secret Insira NS API secret (min 12 caract.) Entregar agora - Limpar fila de espera + Limpar fila Mostrar fila Fila: Status: - Limpar Log + Limpar registros NSCLIENT não tem permissão de escrita. Senha da API errada? Definições Wear - Mostrar IOB detalhado - Dividir IOB entre IOB de bolus e de basal na face do relógio + Mostrar detalhes da IA + Dividir IA entre IA de bolus e de basal na face do relógio não foi bem sucedido - por favor, verifique o telefone n/a Tipo de paciente Criança Adolescente Adulto - Adulto resistente insulina + Adulto resistente à insulina Grávida Selecione o tipo de paciente para configurar os limites de segurança Nome do Paciente Por favor, forneça nome do paciente ou apelido para diferenciar entre várias configurações Usuário Glimp - %1$s necessita de autorizar a não optimização da bateria para assegurar a performance necessária + %1$s necessita autorização de execução sem otimização da bateria para assegurar a performance necessária Loop suspenso - Suspendido (%1$d m) + Suspenso (%1$d m) Suspender loop por 1h Suspender loop por 2h Suspender loop por 3h Suspender loop por 10h Bomba Desconectada - Desligar bomba por 15 min - Desligar bomba por 30 min - Desligar bomba por 1 h - Desligar bomba por 2 h - Desligar bomba por 3 h + Desconectar bomba por 15 min + Desconectar bomba por 30 min + Desconectar bomba por 1 h + Desconectar bomba por 2 h + Desconectar bomba por 3 h 15 min 30 min 1 hora @@ -356,15 +356,15 @@ Loop suspenso Loop retomado Tendência 15 min - COB - Superbólus - Registar inicio da app no NS - A sair da aplicação para aplicar as configurações. - Qual o tipo de insulina que está a utilizar? + CA + Superbolus + Registrar início do app no NS + Saindo do app para aplicar as configurações. + Que tipo de insulina está usando? Novorapid, Novolog, Humalog Fiasp INS - Ativar superbólus no assistente + Ativar superbolus no assistente Habilite a funcionalidade de superbolus no assistente. Não habilite até que aprenda o funcionamento. PODE CAUSAR OVERDOSE DE INSULINA SE USAR INDISCRIMINADAMENTE! Mostrar luzes de estado no ecrã principal Aviso de limite da vida útil da cânula [h] @@ -377,14 +377,14 @@ Aviso de limite crítico do nível da bateria do sensor [%] Aviso de limite da vida útil da bateria da bomba [h] Aviso de limite crítico da vida útil da bateria da bomba [h] - Limite de aviso de nível de reservatório [U] - Limite crítico de nível de reservatório [U] + Aviso de limite de nível de reservatório [U] + Aviso de limite crítico de nível de reservatório [U] ACT Sobre Falta de permissão SMS Falta permissão do estado do telefone xds - Mostrar BGI + Mostrar IG (Impacto na Glicemia) Adicionar BGI à linha de status Cancelar Bólus Estendido Fazer Mudança De Perfil diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 749533d790..97a4cb7739 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -442,6 +442,7 @@ Detekcia neoznámených jedál Čas vrcholu IOB krivky Vrchol krivky [min] + Vrchol Voliteľný vrchol - Oref Rýchlo pôsobiaci - Oref Ultra rýchly - Oref diff --git a/automation/src/main/res/values-cs-rCZ/strings.xml b/automation/src/main/res/values-cs-rCZ/strings.xml index 937c753bd1..d7425d45a3 100644 --- a/automation/src/main/res/values-cs-rCZ/strings.xml +++ b/automation/src/main/res/values-cs-rCZ/strings.xml @@ -120,4 +120,7 @@ Opravdu chcete odstranit %1$d položek Seřadit Automatizace systému + Spustit automatizace + Přidat pravidlo + Odstranit/řadit diff --git a/automation/src/main/res/values-da-rDK/strings.xml b/automation/src/main/res/values-da-rDK/strings.xml index 3c974ffccd..9e3bf9669b 100644 --- a/automation/src/main/res/values-da-rDK/strings.xml +++ b/automation/src/main/res/values-da-rDK/strings.xml @@ -81,6 +81,7 @@ WiFi SSID %1$s %2$s Autosens %1$s %2$s %% Autosens % + Auto sens %3$s %1$s %2$s BS forskel BS difference [%1$s] @@ -112,4 +113,14 @@ Automatiserings event Omorganisering Brugerhandling + Fjern automatisering + Sortér automatisering + Fjern valgte emner + %1$d valgt + Er du sikker på, at du vil fjerne %1$d elementer? + Sortér + System automatisering + Kør automatisering + Tilføj regel + Fjern/sorter diff --git a/automation/src/main/res/values-fr-rFR/strings.xml b/automation/src/main/res/values-fr-rFR/strings.xml index 61475d754a..7456a9452d 100644 --- a/automation/src/main/res/values-fr-rFR/strings.xml +++ b/automation/src/main/res/values-fr-rFR/strings.xml @@ -120,4 +120,6 @@ Êtes-vous sûr de vouloir supprimer %1$d entrée(s) Trier Système d\'automatisation + Ajouter une règle + Supprimer/trier diff --git a/combo/src/main/res/values-da-rDK/strings.xml b/combo/src/main/res/values-da-rDK/strings.xml index c3fa81c46f..d64da89d51 100644 --- a/combo/src/main/res/values-da-rDK/strings.xml +++ b/combo/src/main/res/values-da-rDK/strings.xml @@ -51,4 +51,10 @@ TBR antal Bolus stoppet Stopper bolus + Komm. Fejlantal + Vis komm. fejltælling + Viser antal fejl, når du kommunikerer med Ruffy. I de fleste tilfælde betyder tal højere end 0 at Ruffy oplever kommunikationsproblemer (genstart kan være nødvendigt). + Aldrig + Ved Fejl + Altid diff --git a/core/src/main/res/values-cs-rCZ/strings.xml b/core/src/main/res/values-cs-rCZ/strings.xml index b968a94c54..e7e0c64063 100644 --- a/core/src/main/res/values-cs-rCZ/strings.xml +++ b/core/src/main/res/values-cs-rCZ/strings.xml @@ -455,7 +455,6 @@ Autotune nebude ladit cirkadiánní rozložení, tato možnost použije pouze průměrné hodnoty IC a ISF na váš cirkadiánní vstupní profil Zahrnout další informace o protokolu pro ladění Zapněte, pouze pokud je to požadováno vývojářem, aby bylo možné odeslat další logy pro ladění pluginu Autotune - Výchozí počet dní, ze kterých mají být data zpracována Autotune (až xx) Vyladěno Profil : Ladit dní: @@ -464,7 +463,6 @@ Vyberte profil pro ladění Autotune funguje pouze s jedinou hodnotou IC, váš profil má %1$d hodnot. Průměrná hodnota je %2$.2fg/U Autotune funguje pouze s jedinou hodnotou ISF, váš profil má %1$d hodnot. Průměrná hodnota je %2$.1f%3$s/U - Chyba vstupních dat, zkuste snížit počet dní Autotune spuštěno, prosím buďte trpěliví Před použitím výsledky pečlivě zkontrolujte! Částečný výsledek - vyladěn den %1$d / %2$d diff --git a/core/src/main/res/values-da-rDK/strings.xml b/core/src/main/res/values-da-rDK/strings.xml index 2336188d6b..8f64c75dee 100644 --- a/core/src/main/res/values-da-rDK/strings.xml +++ b/core/src/main/res/values-da-rDK/strings.xml @@ -4,6 +4,7 @@ Opdatér Fejl + Gem Ikke angivet Opdatering af basal profil mislykkedes Basal profil i pumpen er opdateret @@ -41,6 +42,7 @@ Kulhydrater Ugyldig profil !!! INGEN PROFIL SAT + ]]> Dato Enheder DIA @@ -110,6 +112,7 @@ Bemærkninger Fjern Tilføj ny + Tilføj ny ovenfor Data kommer fra en anden pumpe. Skift pumpedriver for at nulstille pumpetilstand. Begrænser max IOB til %1$.2f E/t på grund af %2$s @@ -132,7 +135,9 @@ Placeringen skal være aktiveret, hvis Bluetooth-opdagelsen skal fungere på nyere enheder. AAPS sporer ikke din placering, og det kan deaktiveres efter parring er lykkedes. Forkert kodeord + Forkert pinkode Kodeordene stemmer ikke overens + PIN-koder ikke identiske Basalværdier ikke angivet i hele timer: %1$s Basal dosis erstattet af minimal understøttet dosis: %1$s @@ -352,6 +357,7 @@ STAT NULSTIL SLET LOGS SLET FREMTIDIGE BEHANDLINGER + Slet fremtidige behandlinger EKSPORTER INDSTILLINGER IMPORTER INDSTILLINGER NULSTIL DATABASER @@ -423,7 +429,61 @@ Bolus OK Pumpe parret Insight Opdater-knap + fald %1$s af %2$s + stigning %1$s af %2$s + %1$.0f%% + Basal + Basal % + %1$d valgt + Sortér + Fjern Emner + Sortér Emner + Fjern valgte emner + fil + bruger + Autotune + Hjælp til potentielle justeringer af profil (ISF, KH-ratio og basalrater) + AT + Autotune indstillinger + Automatisk profilskift + Hvis aktiveret, vil Autotune automatisk opdatere og skifte til input-profil efter beregning ud fra en automatiseringsregel. + Kategoriser UAM som basal + Aktiver kun hvis du har indtastet alle kulhydrater på pålidelig vis. Med denne indstilling vil pludselige stigninger set af Autotune, blive brugt til at anbefale ændringer af basal rate. + Antal dage med data + Anvend gennemsnitligt resultat i døgnbaseret IC/ISF + Autotune vil ikke justere døgnrytme variationer, denne indstilling benytter gennemsnitlige værdier til justering af IC og ISF til din døgnrytme input profil + Inkludér flere logoplysninger for fejlfinding + Aktivér kun hvis udviklerne beder dig om det, for at sende flere logoplysninger til at hjælpe med at fejlfinde Autotune plugin + Justeret + Profil : + Justerings dage : + Sidst kørt : + Advarsel : + Vælg profil, der skal justeres + Autotune fungerer med kun én IC-værdi, din profil har %1$d værdier. Gennemsnitsværdi er %2$.2fg/E + Autotune virker med kun én ISF-værdi, din profil har %1$d værdier. Gennemsnitlig værdi er %2$.1f%3$s/E + Automatisk beregning startet, vær venligst tålmodig + Kontrollér resultaterne omhyggeligt, før du bruger dem! + Delvis resultat dag %1$d / %2$d justeret + Resultat: %1$s + Parameter + % + Mangler + Autotune profil %1$s + Kør Autotune + Tjek input-profil + Sammenlign profiler + Kopier til lokal profil + Opdater input profil + Gendan input profil + Opret en ny lokal profil ud fra denne Autotune profil? + Opdater %1$s profil med Autotune profil? + Gendan %1$s profil med Input-profilen? + Profil ugyldig + Autotune kørt uden profilskift + Autotune kørt, profilen er automatisk skiftet + Fejl under sidste Autotune kørsel %1$d dag %1$d dage diff --git a/core/src/main/res/values-es-rES/strings.xml b/core/src/main/res/values-es-rES/strings.xml index 5c90e29f8b..a2fc66be66 100644 --- a/core/src/main/res/values-es-rES/strings.xml +++ b/core/src/main/res/values-es-rES/strings.xml @@ -455,7 +455,6 @@ Autotune no afinará las variaciones circadianas, esta opción solo aplica los ajustes medios de IC e ISF a tu perfil circadiano de entrada Incluir más información de registro para depuración Activar sólo si es solicitado por los desarrolladores, para enviar más registros y así ayudar a depurar el plugin Autotune - Número predeterminado de días de datos a procesar por Autotune (hasta xx) Ajustado Perfil : Días de ajuste: @@ -464,7 +463,6 @@ Selecciona el perfil para a ajustar Autotune sólo funciona con un valor de IC. Tu perfil tiene %1$d valores. El valor promedio es %2$.2fg/U Autotune sólo funciona con un valor de ISF. Tu perfil tiene %1$d valores. El valor promedio es %2$.1f%3$s/U - Error en los datos de entrada, intenta reducir el número de días Cálculo de autototune iniciado, por favor ten paciencia ¡Comprueba los resultados cuidadosamente antes de usarlos! Resultado parcial día %1$d / %2$d afinado diff --git a/core/src/main/res/values-fr-rFR/strings.xml b/core/src/main/res/values-fr-rFR/strings.xml index c7bec5ee4c..91ec89634c 100644 --- a/core/src/main/res/values-fr-rFR/strings.xml +++ b/core/src/main/res/values-fr-rFR/strings.xml @@ -455,7 +455,6 @@ Autotune ne réglera pas les variations circadiennes, cette option ne fait que appliquer le réglage moyen du G/I et de la SI à votre profil d\'entrée circadien Inclure plus d\'informations de log pour le débogage A n\'activer que sur demande du développeur pour envoyer plus d\'informations dans les fichiers log pour aider à déboguer le plugin Autotune - Nombre de jours de données par défaut à traiter par Autotune (jusqu\'à 30) Tuned Profil : Nb jours : @@ -464,7 +463,6 @@ Sélectionnez le profil à optimiser Autotune ne fonctionne qu\'avec une seule valeur G/I, votre profil a %1$d valeurs. La valeur moyenne est de %2$.2f g/U Autotune ne calcule qu\'une seule valeur de SI, votre profil a %1$d valeurs. La valeur moyenne est de %2$.1f %3$s/U - Erreur dans les données d\'entrée, essayez de relancer le calcul ou réduire le nombre de jours Le calcul Autotune a commencé, veuillez patienter Vérifiez attentivement les résultats avant de les utiliser! Résultat partiel jour %1$d / %2$d calculé diff --git a/core/src/main/res/values-no-rNO/strings.xml b/core/src/main/res/values-no-rNO/strings.xml index a33dc1c80e..a8ad8355f8 100644 --- a/core/src/main/res/values-no-rNO/strings.xml +++ b/core/src/main/res/values-no-rNO/strings.xml @@ -42,6 +42,7 @@ Karbohydrater Ugyldig profil!!! INGEN PROFIL VALGT + ]]> Dato Enheter DIA @@ -441,6 +442,48 @@ fil bruker + Autotune + Hjelp til å justere profilen (insulinfølsomhet, karbohydratfaktor og basal doser) + AT + Autotune innstillinger + Automatisering profilbytte + Hvis det er aktivert så vil Autotune automatisk oppdatere profilen og skifte til denne etter beregninger fra en automatiserings regel. + Kategoriser UAM (uannonsert måltid) som basal + Aktiver kun hvis du korrekt har angitt alle spiste karbohydrater. Med dette alternativet vil Autotune foreslå endringer i basal doser hvis den plutselig oppdager BS stigninger. + Antall dager med data + Bruk gjennomsnittsverdier i cirkadisk IK/ISF + Autotune vil ikke justere cirkadiske variasjoner. Dette valget benytter gjennomsnittsverdier til å justere IK og ISF i din cirkadiske inngangsprofil + Inkluder mer logginformasjon for feilsøking + Slå på kun dersom det er forespurt av utviklere for å sende mer logginformasjon for å hjelpe i feilsøking av Autotune + Innstilt + Profil : + Antall dager: + Siste beregning : + Varsel: + Velg profil du vil justere + Autotune fungerer bare med én IC-verdi, og din profil har %1$d verdier. Gjennomsnittlig verdi er %2$.2fg/E + Autotune fungerer med bare én ISF verdi, og din profil har %1$d verdier. Gjennomsnittsverdi er %2$.1f%3$s/E + Har startet Autotune beregning, vennligst vent + Kontroller resultatene nøye før du bruker dem! + Delvis resultat dag %1$d / %2$d justert + Resultat: %1$s + Parametre + % + Mangler + Autotune profil %1$s + Kjør Autotune + Sjekk inngangsprofil + Sammenlign profiler + Kopier til lokal profil + Oppdater inngangsprofil + Tilbakestill inngangsprofil + Opprette ny lokal profil fra denne Autotune profilen? + Oppdater %1$s profil med Autotune profil? + Tilbakestill %1$s profil med inngangsprofil? + Ugyldig profil + Autotune utført uten profilbytte + Autotune utført og profil automatisk skiftet ut + Feil oppdaget under siste Autotune kjøring %1$d dag %1$d dager diff --git a/core/src/main/res/values-sk-rSK/strings.xml b/core/src/main/res/values-sk-rSK/strings.xml index 73ecf80e3b..780625bd0a 100644 --- a/core/src/main/res/values-sk-rSK/strings.xml +++ b/core/src/main/res/values-sk-rSK/strings.xml @@ -450,12 +450,14 @@ Pokiaľ je táto voľba povolená, Autotune po výpočte automaticky aktualizuje a prepne na vstupný profil zadaný v pravidle automatizácie. Započítavať UAM ako bazál Povoľte iba v prípade, že ste dôsledne zadávali všetky prijaté sacharidy. S touto možnosťou bude náhly vzostup detekovaný funkciou Autotune, použitý pre doporučovanie zmien bazálu. + Ladiť inzulínovú krivku + Aktivuj iba ak používaš voliteľný vrchol. Táto možnosť má vyladiť vrchol a trvanie DIA Počet dní s dátami Použi priemerný výsledok v cirkadiánnom IC/ISF Autotune nebude ladiť cirkadiánne rozloženie, táto možnosť použije iba priemerné ladenie IC a ISF na váš cirkadiánný vstupní profil Zahrnúť ďalšie informácie o protokole pre ladenie Zapnite, iba pokiaľ je to požadované vývojárom, aby bolo možné odoslať dalšie logy pre ladenie modulu Autotune - Štandardný počet dní, z ktorých majú byť dáta spracované Autotune (až do xx) + Štandardný počet dní, z ktorých majú byť dáta spracované Autotune (až do 30) Vyladené Profil : Ladiť dni: @@ -464,7 +466,7 @@ Vyberte profil pre ladenie Autotune funguje iba s jedinou hodnotou IC, váš profil má %1$d hodnôt. Priemerná hodnota je %2$.2fg/JI Autotune funguje iba s jedinou hodnotou IC, váš profil má %1$d hodnôt. Priemerná hodnota je %2$.1fg/JI - Chyba vstupných dát, skúste znížiť počet dní + Chyba vstupných dát, skúste znova spustiť Autotune, alebo znížte počet dní Autotune spustený, prosím buďte trpezliví Pred použitím výsledky starostlivo skontrolujte! Čiastočný výsledok - vyladený deň %1$d / %2$d diff --git a/core/src/main/res/values-tr-rTR/strings.xml b/core/src/main/res/values-tr-rTR/strings.xml index 1a20d33564..254f9d6cc5 100644 --- a/core/src/main/res/values-tr-rTR/strings.xml +++ b/core/src/main/res/values-tr-rTR/strings.xml @@ -455,7 +455,6 @@ OtoAyar sirkadiyen varyasyonları ayarlamaz, bu seçenek sirkadiyen giriş profilinize yalnızca IC ve İDF\'nün ortalama ayarını uygular Hata ayıklama için daha fazla günlük bilgisi ekleyin OtoAyar eklentisinde hata ayıklamaya yardımcı olmak için yalnızca geliştirici tarafından daha fazla günlük bilgisi göndermesi istenirse açın - OtoAyar tarafından işlenecek varsayılan veri gün sayısı (en fazla xx) Ayarlandı Profil : Ayar günleri: @@ -464,7 +463,6 @@ Ayarlanacak profili seçin OtoAyar yalnızca bir IC değeriyle çalışır, profilinizde %1$d değer mevcut. Ortalama değer: %2$.2fg/Ü OtoAyar yalnızca bir İDF değeriyle çalışır, profilinizde %1$d değer mevcut. Ortalama değer: %2$.1f%3$s/Ü - Giriş verilerinde hata, gün sayısını azaltmaya çalışın OtoAyar hesaplaması başladı, lütfen sabırlı olun Kullanmadan önce sonuçları dikkatlice kontrol edin! Kısmi sonuç günü %1$d / %2$d ayarlandı diff --git a/dana/src/main/res/values-da-rDK/strings.xml b/dana/src/main/res/values-da-rDK/strings.xml index 2b07d2df19..74e931758c 100644 --- a/dana/src/main/res/values-da-rDK/strings.xml +++ b/dana/src/main/res/values-da-rDK/strings.xml @@ -1,5 +1,120 @@ + Parring + Ingen enhed fundet indtil videre + Parring OK + Parring fik timeout + Venter på parring på pumpen + Dana-i/RS + Dana + Pumpeintegration til DANA Diabecare RS og Dana-i pumper + Max bolus overtrædelse + Kommando fejl + Hastighedsfejl + Overtrædelse af insulingrænse + Ønsket: %1$.2fU Afgivet: %2$.2fU Fejlkode: %3$s + Værdi ikke angivet korrekt + Indstil basal trin til 0,01 E/t + Nulstil parringsinformation? + %1$s\nModel: %2$02X\nProtokol: %3$02X\nKode: %4$02X + Behandler begivenhed + Aktiver forlængede bolusser på pumpe + Afgivet + Stoppet + Ikke understøttet pumpe firmware + Pumpe Fejl + Lavt batteri + Leverer mindre end forudindstillet basal rate + Pumpe Nedlukning + Pumpe batteri afladet + Tilstopning + Tomt reservoir + Kontrollér aksel + Basal max + Daglig max + Advarsel om måling af blodsukker + Resterende insulin niveau + Glemt bolus + Ugyldig parringsinformation. Anmoder om ny parring + Henter pumpestatus + Henter forlænget bolusstatus + Henter bolus status + Få midlertidig basal status + Henter pumpeindstillinger + Henter pumpetid + Stor Tidsforskel + Stor tidsforskel:\nTid i pumpen varierer med mere end 1,5 time.\nJustér venligst tiden manuelt på pumpen og sørg for, at læsning af historikken fra pumpen ikke forårsager uventet adfærd.\nHvis det er muligt; fjern historik fra pumpen før du ændrer tiden eller deaktiverer closed loop i én DIA efter den sidste forkerte historik post, men minimum én DIA fra nu. + Par venligst din pumpe med din telefon! + Nærmer sig daglig insulingrænse + Starter bolus afgivelse + Venter på bolusafslutningen. Mangler %1$d sek. + Stopper midlertidig basal + Indstiller forlænget bolus + Stopper forlænget bolus + Opdaterer basal rater + Indstiller midlertidig basal + Venter på tidssynkronisering (%1$d sek) + Forkert pumpe kodeord! + Alarmer + Basal Timer + Bolusser + Kulhydrater + Daglig insulin + Fejl Glukose + Genopfyld + Suspendér + Klargør + Brugerindstillinger + Visning af tidsformat + Knap rulning + Bip ved tryk på knap Alarm + Lyd + Vibrér + Begge + LCD tændt tid [seconds] + Bagbelysning tændt tid [seconds] + Glukose enheder + Nedlukning [hours] + Lavt reservoir [Units] + Gem indstillinger på pumpen + Pumpeintegration til DANA Diabecare R pumper + Pumpeintegration til DANA Diabecare R pumper, koreansk version + Pumpeintegration til DANA Diabecare R pumper med opgraderet firmware (v2) + DANA + Ingen bluetooth-adapter fundet + Valgte enhed ikke fundet + Skift tilstand fra E/d til E/t på pumpen + DanaR Koreansk + DanaR + Pumpe driver korrigeret + DanaRv2 + Deaktivér EasyUI-tilstand i pumpen + Indstilling af basal profil mislykkedes + Bluetooth Status + Pumpe IOB + Firmware + Dana pumpe indstillinger + 12t + 24t + Tændt + Slukket + DanaR Bluetooth enhed + Pumpe kodeord (kun v1) + Pumpe kodeord + Brug forlængede bolusser til >200%% + Bolus hastighed + Valgt pumpe + Log reservoir ændring + Tilføj \"Insulin Skift\" begivenhed til careportal, når de registreres i historik + Log indstik skift + Tilføj \"Indstik skift\" begivenhed til careportal, når de registreres i historik + PIN1 + PIN2 + Tryk OK på pumpen\nog angiv 2 viste numre\nHold skærmen tændt på pumpen ved at trykke på minus knappen, indtil du er færdig med at indtaste kode. + 1: (12 cifre) + 2: (8 cifre) + Basal/bolus trin + Kulhydrater blev sandsynligvis ikke gemt korrekt. Tjek manuelt og gem igen, hvis det er nødvendigt. diff --git a/diaconn/src/main/res/values-da-rDK/strings.xml b/diaconn/src/main/res/values-da-rDK/strings.xml index e0b3053560..94d0c7de05 100644 --- a/diaconn/src/main/res/values-da-rDK/strings.xml +++ b/diaconn/src/main/res/values-da-rDK/strings.xml @@ -1,6 +1,157 @@ + Nulstil parring + Ingen enhed tilgængelig + Diaconn Pumpe Parring + Pumpe Fejl + Alarmer + Basal Timer + Bolusser + Daglig insulin + Fejl + Klargør + Genopfyld + Suspendér + Parring OK + Venter på parring + Version + Ugyldig parringsinformation. Anmoder om ny parring + Henter pumpeindstillinger + Henter pumpetid + Stor tidsforskel:\nTid i pumpen varierer med mere end 1,5 time.\nJustér venligst tiden manuelt på pumpen og sørg for, at læsning af historikken fra pumpen ikke forårsager uventet adfærd.\nHvis det er muligt; fjern historik fra pumpen før du ændrer tiden eller deaktiverer closed loop i én DIA efter den sidste forkerte historik post, men minimum én DIA fra nu. + Stor tidsforskel + Nærmer sig daglig insulingrænse + Starter bolus afgivelse + Venter på estimerede bolusafslutning + Henter bolus status + Stopper midlertidig basal + Indstiller midlertidig basal + Indstiller forlænget bolus + Stopper forlænget bolus + Opdaterer basal rater + Pumpeintegration til Diaconn G8 Pumper + Diaconn G8 + Diaconn G8 + Max bolus overtrædelse + Kommando fejl + Hastighedsfejl + Overtrædelse af insulingrænse + Ønsket: %1$.2fU Afgivet: %2$.2fU Fejlkode: %3$s + Værdi ikke angivet korrekt + Diaconn G8 Bluetooth enhed + Pumpe kodeord + Bolus hastighed + Valgt pumpe + Brug forlængede bolusser til >200%% + Vis forlænget bolus som %% + Bluetooth Status + TDD + Bolus Trin + Basal Trin + Firmware + BRUGERINDSTILLINGER + Par venligst din pumpe med din telefon! + "Behandler begivenhed " + aps_last_log_num + aps_wrapping_count + Midlertidig basal + Diaconn pumpe indstillinger + Lyd + lyd + vibrér + lydløs + Alarm intensitet lav + middel + høj + LCD tændt tid [second] + GEM INDSTILLING TIL PUMPE + Sprog + Bolus hastighed + Kinesisk + Koreansk + Engelsk + "10 " + 10 + 20 + 30 lav + Injektion blokkeret + Batteri Advarsel + Lavt insulin niveau + Batteriudskiftning er påkrævet + Insulin udskiftning er påkrævet + pump_version + aps_incarnation_no + pump_serial_no + Log reservoir skift + Tilføj \"Insulin Skift\" begivenhed til careportal, når de registreres i historik + Log indstik skift + Tilføj \"Indstik skift\" begivenhed til careportal, når de registreres i historik + Tilføj \"Batteri skift\" begivenhed til careportal, når det registreres i historik + Log skift af batteri + Synkronisering af log i gang + Lavt insulinniveau + Lavt batteriniveau + Nåle fyldning: %1$.2fIE + Forfyldning: %1$.2fIE + Slange fyldning: %1$.2fIE + Nulstil efter fabriksnulstilling + Nulstil efter nødstilstand + Nulstil efter udskiftning af batteri + Nulstil efter kalibrering + Nulstil til fabriksindstillinger + Uventet systemnulstilling + Fuldført + Injektion blokkeret + Lavt batteriniveau + Lavt insulinniveau + Stoppet af bruger + Nulstil system Andet + Nødstop + BASAL + MÅLTIDS BOLUS + NORMAL BOLUS + FORLÆNGET BOLUS + KOMBINERET BOLUS + UDSKIFT SLANGE + UDSKIFT KANYLE + UDSKIFT SPRØJTE + Injektion blokkeret (%s) + basal afgivelse (%s) + basal suspendering (%s) + Normal kombinationsbolus vellykket + Kombineret bolus startet + Kombineret bolus vellykket + Forlænget bolus startet + Forlænget bolus vellykket + Måltid Mislykkedes + Succes + Måltid Gennemført + Kan ikke kontrollere pga. pakke CRC fejl. + Kan ikke indstilles pga. en inputparameter fejl. + Kan ikke indstilles pga. en protokol specifikationsfejl. + Måltid registreret, kan ikke afgive. + Annulleret af pumpen + Tager andre handlinger, begrænser app-indstillinger. + Under en anden Bolus-injektion er injektioner begrænsede. + Kræver genoptagelse af basal afgivelse + Annulleret pga. manglende respons på pumpen. + Injektion er ikke mulig pga. lavt batteri. + Insulinmangel, kan ikke afgives. + Du kan ikke afgive, da det overskrider grænsen. + Injektionen kan ikke afgives, da den overstiger max. daglig dosis. + Efter basal opsætning er færdig, kan basal injektion udføres. + Kommandoen blev ikke leveret. Prøv igen. + Log skift af slange + Tilføj \"Slange skift\" begivenhed til careportal, når de registreres i historik + Midlertidig basal start + Under LGS er injektionen begrænset + LGS-status er allerede TIL, kommando afvist. + LGS-status er allerede FRA, kommando afvist. + Midlertidig basal kan ikke startes fordi en anden midlertidig basal er i gang + Midlertidig basal kan ikke stoppes fordi en midlertidig basal ikke er i gang + Send pumpelogs til Diaconn Cloud. + Diaconn Cloud Synkronisering diff --git a/diaconn/src/main/res/values-no-rNO/strings.xml b/diaconn/src/main/res/values-no-rNO/strings.xml index 2c33d1b032..6ede995fb4 100644 --- a/diaconn/src/main/res/values-no-rNO/strings.xml +++ b/diaconn/src/main/res/values-no-rNO/strings.xml @@ -152,4 +152,6 @@ LGS status er AV. AV kommandoen er avslått. Temp basal avslått siden det allerede kjøres en temp basal Stopp av temp basal avslått siden det ikke kjøres en temp basal + Send pumpe logger til Diaconn Cloud. + Diaconn Cloud Sync diff --git a/medtronic/src/main/res/values-da-rDK/strings.xml b/medtronic/src/main/res/values-da-rDK/strings.xml index 6f2163fe40..a2683669d9 100644 --- a/medtronic/src/main/res/values-da-rDK/strings.xml +++ b/medtronic/src/main/res/values-da-rDK/strings.xml @@ -91,4 +91,6 @@ Hvis aktiveret, vil det annullere en midlertidig basal inden udgangen af hver time. Denne metode kan hjælpe med at stoppe nogle pumper i at bippe/vibrere hver time. %1$.1f E/t (%2$d min. tilbage) Ugyldig pumpehistorikdata registreret. Åbn nyt problem og upload logfiler. + RL Statistik + Type: diff --git a/rileylink/src/main/res/values-da-rDK/strings.xml b/rileylink/src/main/res/values-da-rDK/strings.xml index 0168af7554..4fc251483d 100644 --- a/rileylink/src/main/res/values-da-rDK/strings.xml +++ b/rileylink/src/main/res/values-da-rDK/strings.xml @@ -77,4 +77,5 @@ Brug Scanning Scan før du opretter forbindelse til OrangeLink, det bør forbedre forbindelsen (kan også bruges med andre RileyLink kloner, hvis nødvendigt) + RileyLink Konfiguration diff --git a/wear/src/main/res/values-da-rDK/strings.xml b/wear/src/main/res/values-da-rDK/strings.xml index 675ce0b8c7..dbb73b2586 100644 --- a/wear/src/main/res/values-da-rDK/strings.xml +++ b/wear/src/main/res/values-da-rDK/strings.xml @@ -2,6 +2,18 @@ AAPS AAPS + AAPS + AAPS (Stor) + AAPS(StorGraf) + AAPS(IngenGraf) + AAPS(Cirkel) + AAPS(v2) + AAPS(Cockpit) + AAPS(Steampunk) + AAPS (DigitalStil) + AAPS (Handlinger) + AAPS(Midl. Mål) + AAPS(Hurtig Guide) Ingen data! Gammel data! Siden %1$s @@ -45,14 +57,23 @@ Ring Historik Lys Ring Historik Animationer + Beregner i Menu Prime i menuen Enkelt Mål + Beregner Procent Complication handling ved tryk Unicode i komplikationer Version: + Flere urskive indstillinger Se venligst på Watchface-konfigurationen. MidlertidigMål + Beregner + Beregn + Behandling + Behandl Bolus + Kulhydrater + Forlængede kulhydrater Indstillinger Status Gensynkronisering @@ -60,17 +81,38 @@ Ingen Standard Menu + XL + Varighed + Midlertidig Mål Anmodet + Hurtig Guide Anmodet + Behandling Anmodet + Bolus Anmodet + Beregning Anmodet + Fyldning Anmodet + Kulhydrater Anmodet + Profilskift anmodet + Mål + Lav + Høj + Kulhydrater + Forlængede kulhydrater + Procentdel + Start [min] + Varighed [h] + Insulin Forvalg 1 Forvalg 2 Forvalg 3 Fri mængde BEKRÆFT tidsforskydning + Bolus Bolus Fremskridt tryk for at annullere ANNULLER BOLUS Pumpe Loop + Profilskift TDD Kulhydrat IOB @@ -117,4 +159,13 @@ Under Opladning Altid Tændt Tilstand Altid Tændt ved opladning + Spiser + Hypo + Aktivitet + Manuel + Annuller + Ingen + Ingen konfiguration tilgængelig + Wear kontrol deaktiveret + Ingen data tilgængelig diff --git a/wear/src/main/res/values-no-rNO/strings.xml b/wear/src/main/res/values-no-rNO/strings.xml index c2d0f8bf94..91483daf3a 100644 --- a/wear/src/main/res/values-no-rNO/strings.xml +++ b/wear/src/main/res/values-no-rNO/strings.xml @@ -2,6 +2,15 @@ AAPS AAPS + AAPS + AAPS (stor) + AAPS (stor graf) + AAPS (ingen graf) + AAPS (sirkel) + AAPS(v2) + AAPS(Cockpit) + AAPS(Steampunk) + AAPS(Digitalstil) AAPS(Actions) AAPS(Temp Target) AAPS(Quick Wizard) @@ -55,6 +64,7 @@ Komplikasjon trykk handling Unicode ikomplikasjoner Versjon: + Fler urskive innstillinger Vennligst sjekk urskive innstillinger. TempT Kalkulator @@ -80,6 +90,7 @@ Beregning forespurt Fylling forespurt Karbo forespurt + Profilbytte forespurt Mål Lav Høy @@ -101,6 +112,7 @@ AVBRYT BOLUS Pumpe Loop + Profilbytte TDD Karbo IOB From 4e440550143f05271a489f16359532d5f82a9058 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 10 May 2022 18:24:03 +0200 Subject: [PATCH 18/18] Automation: allow 50% profile --- .../plugins/general/automation/elements/InputPercent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputPercent.kt b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputPercent.kt index 61cfd2b550..91032dec63 100644 --- a/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputPercent.kt +++ b/automation/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputPercent.kt @@ -25,7 +25,7 @@ class InputPercent() : Element() { companion object { - const val MIN = 70.0 + const val MIN = 50.0 const val MAX = 130.0 } } \ No newline at end of file