Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ wear/pumpcontrol/*
.claude/settings.local.json
.claude/CLAUDE_COMMANDS.md
tmpclaude-*
build_output*.txt
1 change: 1 addition & 0 deletions app/src/main/kotlin/app/aaps/ComposeMainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@
* No `else` — compiler catches missing enum values.
*/
private fun navigateToElement(elementType: ElementType, navController: NavController, mode: ScreenMode = ScreenMode.EDIT) {
when (elementType) {

Check warning on line 1032 in app/src/main/kotlin/app/aaps/ComposeMainActivity.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the number of when branches from 40 to at most 30.

See more on https://sonarcloud.io/project/issues?id=nightscout_AndroidAPS&issues=AZ6KguYUsynw1OJ1GdZ0&open=AZ6KguYUsynw1OJ1GdZ0&pullRequest=4877
// Navigation screens (drawer)
ElementType.TREATMENTS -> navController.navigate(AppRoute.Treatments.route)
ElementType.STATISTICS,
Expand All @@ -1055,6 +1055,7 @@
// Treatment dialogs
ElementType.CARBS -> navController.navigate(AppRoute.CarbsDialog.route)
ElementType.INSULIN -> navController.navigate(AppRoute.InsulinDialog.route)
ElementType.AFREZZA -> navController.navigate(AppRoute.AfrezzaDialog.route)
ElementType.TREATMENT -> navController.navigate(AppRoute.TreatmentDialog.route)
ElementType.FILL -> navController.navigate(AppRoute.FillDialog.createRoute(FillPreselect.CARTRIDGE_CHANGE.ordinal))
ElementType.CANNULA_CHANGE -> navController.navigate(AppRoute.FillDialog.createRoute(FillPreselect.SITE_CHANGE.ordinal))
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/kotlin/app/aaps/compose/navigation/AppNavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import app.aaps.ui.compose.configuration.ConfigurationViewModel
import app.aaps.ui.compose.extendedBolusDialog.ExtendedBolusDialogScreen
import app.aaps.ui.compose.fillDialog.FillDialogScreen
import app.aaps.ui.compose.history.HistoryScreen
import app.aaps.ui.compose.afrezzaDialog.AfrezzaDialogScreen
import app.aaps.ui.compose.insulinDialog.InsulinDialogScreen
import app.aaps.ui.compose.insulinManagement.InsulinManagementScreen
import app.aaps.ui.compose.insulinManagement.InsulinManagementViewModel
Expand Down Expand Up @@ -309,6 +310,15 @@ fun NavGraphBuilder.appNavGraph(
)
}

composable(route = AppRoute.AfrezzaDialog.route) {
AfrezzaDialogScreen(
onNavigateBack = { navController.safePopBackStack() },
onShowMessage = { _ ->
// Toast or Snackbar handled by caller
}
)
}

composable(route = AppRoute.TreatmentDialog.route) {
TreatmentDialogScreen(
bgInfoState = graphViewModel.bgInfoState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ sealed class AppRoute(val route: String) {
data object CalibrationDialog : AppRoute("calibration_dialog")
data object CarbsDialog : AppRoute("carbs_dialog")
data object InsulinDialog : AppRoute("insulin_dialog")
data object AfrezzaDialog : AppRoute("afrezza_dialog")
data object TreatmentDialog : AppRoute("treatment_dialog")
data object TempBasalDialog : AppRoute("temp_basal_dialog")
data object ExtendedBolusDialog : AppRoute("extended_bolus_dialog")
Expand Down
1 change: 1 addition & 0 deletions core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.aaps.core.data.ue
enum class Sources {
TreatmentDialog,
InsulinDialog,
AfrezzaDialog,
CarbDialog,
WizardDialog,
QuickWizard,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package app.aaps.core.data.model

import app.aaps.core.data.iob.Iob
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

/**
* Validate the oref bilinear IOB model with Afrezza (Technosphere inhaled insulin) parameters.
*
* Afrezza pharmacokinetics (published):
* - Onset: ~12 minutes
* - Peak (Tmax): 35-45 minutes
* - Clinical duration: 1.5-3 hours (dose-dependent)
*
* Model parameters used:
* - Peak: 40 minutes
* - DIA: 2.5 hours (150 minutes)
* - Concentration: 1.0 (U100 equivalent)
*/
class ICfgAfrezzaIobTest {

private lateinit var afrezzaCfg: ICfg
private lateinit var fiaspCfg: ICfg
private lateinit var bolus1U: BS

@BeforeEach
fun setup() {
afrezzaCfg = ICfg(
insulinLabel = "Afrezza (Inhaled)",
peak = 40, // minutes
dia = 2.5, // hours
concentration = 1.0
)
fiaspCfg = ICfg(
insulinLabel = "Fiasp",
peak = 55, // minutes
dia = 5.0, // hours
concentration = 1.0
)
bolus1U = BS(
timestamp = 0L,
amount = 1.0,
type = BS.Type.NORMAL,
iCfg = afrezzaCfg
)
}

@Test
fun `IOB starts near 1_0 at time zero`() {
// At t=1 minute after bolus, IOB should be very close to 1.0
val iob = afrezzaCfg.iobCalcForTreatment(bolus1U, 60_000L) // 1 min in ms
assertTrue(iob.iobContrib > 0.95, "IOB at t=1min should be >0.95, was ${iob.iobContrib}")
assertTrue(iob.iobContrib <= 1.0, "IOB at t=1min should be <=1.0, was ${iob.iobContrib}")
}

@Test
fun `IOB is zero at DIA`() {
// At t=DIA (150 minutes), IOB should be exactly 0
val iob = afrezzaCfg.iobCalcForTreatment(bolus1U, 150L * 60_000L)
assertEquals(0.0, iob.iobContrib, 0.001, "IOB should be 0 at t=DIA")
assertEquals(0.0, iob.activityContrib, 0.001, "Activity should be 0 at t=DIA")
}

@Test
fun `IOB is zero after DIA`() {
// At t=DIA+30min, IOB should still be 0
val iob = afrezzaCfg.iobCalcForTreatment(bolus1U, 180L * 60_000L)
assertEquals(0.0, iob.iobContrib, 0.001, "IOB should be 0 after DIA")
}

@Test
fun `IOB decreases monotonically after peak`() {
var prevIob = 1.0
// Check from t=50min to t=150min (after peak, IOB should only decrease)
for (t in 50..150 step 5) {
val iob = afrezzaCfg.iobCalcForTreatment(bolus1U, t.toLong() * 60_000L)
assertTrue(
iob.iobContrib <= prevIob + 0.001,
"IOB should decrease monotonically after peak. At t=${t}min: ${iob.iobContrib} > prev: $prevIob"
)
prevIob = iob.iobContrib
}
}

@Test
fun `no NaN or negative values across entire curve`() {
for (t in 0..200 step 1) {
val iob = afrezzaCfg.iobCalcForTreatment(bolus1U, t.toLong() * 60_000L)
assertTrue(!iob.iobContrib.isNaN(), "IOB should not be NaN at t=${t}min")
assertTrue(!iob.activityContrib.isNaN(), "Activity should not be NaN at t=${t}min")
assertTrue(iob.iobContrib >= 0.0, "IOB should not be negative at t=${t}min, was ${iob.iobContrib}")
assertTrue(iob.activityContrib >= 0.0, "Activity should not be negative at t=${t}min, was ${iob.activityContrib}")
}
}

@Test
fun `peak activity occurs near configured peak time`() {
var maxActivity = 0.0
var maxActivityTime = 0
for (t in 1..150) {
val iob = afrezzaCfg.iobCalcForTreatment(bolus1U, t.toLong() * 60_000L)
if (iob.activityContrib > maxActivity) {
maxActivity = iob.activityContrib
maxActivityTime = t
}
}
// Peak should be within +/- 10 minutes of configured peak (40 min)
assertTrue(
maxActivityTime in 30..50,
"Peak activity should occur near t=40min, was at t=${maxActivityTime}min"
)
}

@Test
fun `Afrezza IOB decays faster than Fiasp`() {
val fiaspBolus = BS(timestamp = 0L, amount = 1.0, type = BS.Type.NORMAL, iCfg = fiaspCfg)

// At 90 minutes, Afrezza should have substantially less IOB than Fiasp
val afrezzaIob90 = afrezzaCfg.iobCalcForTreatment(bolus1U, 90L * 60_000L)
val fiaspIob90 = fiaspCfg.iobCalcForTreatment(fiaspBolus, 90L * 60_000L)

assertTrue(
afrezzaIob90.iobContrib < fiaspIob90.iobContrib,
"At t=90min, Afrezza IOB (${afrezzaIob90.iobContrib}) should be less than Fiasp IOB (${fiaspIob90.iobContrib})"
)

// At 150 minutes (Afrezza DIA), Afrezza IOB should be ~0, Fiasp still significant
val afrezzaIob150 = afrezzaCfg.iobCalcForTreatment(bolus1U, 150L * 60_000L)
val fiaspIob150 = fiaspCfg.iobCalcForTreatment(fiaspBolus, 150L * 60_000L)

assertEquals(0.0, afrezzaIob150.iobContrib, 0.001, "Afrezza IOB should be ~0 at t=150min")
assertTrue(
fiaspIob150.iobContrib > 0.05,
"Fiasp IOB should still be significant at t=150min, was ${fiaspIob150.iobContrib}"
)
}

@Test
fun `dose scaling works correctly for cartridge sizes`() {
// 4U, 8U, 12U should produce proportional IOB
val bolus4U = BS(timestamp = 0L, amount = 4.0, type = BS.Type.NORMAL, iCfg = afrezzaCfg)
val bolus8U = BS(timestamp = 0L, amount = 8.0, type = BS.Type.NORMAL, iCfg = afrezzaCfg)
val bolus12U = BS(timestamp = 0L, amount = 12.0, type = BS.Type.NORMAL, iCfg = afrezzaCfg)

val t = 60L * 60_000L // 60 minutes
val iob4 = afrezzaCfg.iobCalcForTreatment(bolus4U, t)
val iob8 = afrezzaCfg.iobCalcForTreatment(bolus8U, t)
val iob12 = afrezzaCfg.iobCalcForTreatment(bolus12U, t)

assertEquals(iob4.iobContrib * 2.0, iob8.iobContrib, 0.001, "8U IOB should be 2x 4U IOB")
assertEquals(iob4.iobContrib * 3.0, iob12.iobContrib, 0.001, "12U IOB should be 3x 4U IOB")
}

@Test
fun `combined Afrezza plus Fiasp IOB sums correctly`() {
// Simulate: Afrezza 8U at t=0, Fiasp pump bolus 3U at t=0
val afrezzaBolus = BS(timestamp = 0L, amount = 8.0, type = BS.Type.NORMAL, iCfg = afrezzaCfg)
val fiaspBolus = BS(timestamp = 0L, amount = 3.0, type = BS.Type.NORMAL, iCfg = fiaspCfg)

val t = 60L * 60_000L // 60 minutes
val afrezzaIob = afrezzaCfg.iobCalcForTreatment(afrezzaBolus, t)
val fiaspIob = fiaspCfg.iobCalcForTreatment(fiaspBolus, t)
val combinedIob = afrezzaIob.iobContrib + fiaspIob.iobContrib

// Combined should equal sum of individual IOBs
assertTrue(combinedIob > 0, "Combined IOB should be positive")
assertTrue(combinedIob < 11.0, "Combined IOB should be less than total dose (11U)")

// At t=180min (well past Afrezza DIA), only Fiasp should remain
val t180 = 180L * 60_000L
val afrezzaIob180 = afrezzaCfg.iobCalcForTreatment(afrezzaBolus, t180)
val fiaspIob180 = fiaspCfg.iobCalcForTreatment(fiaspBolus, t180)

assertEquals(0.0, afrezzaIob180.iobContrib, 0.001, "Afrezza IOB should be 0 at t=180min")
assertTrue(fiaspIob180.iobContrib > 0.1, "Fiasp IOB should still be active at t=180min")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import app.aaps.core.data.model.ICfg
import app.aaps.core.interfaces.R
import app.aaps.core.interfaces.resources.ResourceHelper

enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeakTime: Long, @StringRes val label: Int, @StringRes val comment: Int) {
enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeakTime: Long, @StringRes val label: Int, @StringRes val comment: Int, val isInhaled: Boolean = false) {
UNKNOWN(-1, 0, 0, R.string.unknown, R.string.unknown),

// int FAST_ACTING_INSULIN = 0; // old model no longer available
// int FAST_ACTING_INSULIN_PROLONGED = 1; // old model no longer available
OREF_RAPID_ACTING(2, 8 * 3600 * 1000, 75 * 60000, R.string.rapid_acting_oref, R.string.fast_acting_insulin_comment),
OREF_ULTRA_RAPID_ACTING(3, 8 * 3600 * 1000, 55 * 60000, R.string.ultra_rapid_oref, R.string.ultra_fast_acting_insulin_comment),
OREF_FREE_PEAK(4, 8 * 3600 * 1000, 50 * 60000, R.string.free_peak_oref, R.string.insulin_peak_time),
OREF_LYUMJEV(5, 8 * 3600 * 1000, 45 * 60000, R.string.lyumjev, R.string.lyumjev);
OREF_LYUMJEV(5, 8 * 3600 * 1000, 45 * 60000, R.string.lyumjev, R.string.lyumjev),
OREF_INHALED_AFREZZA(6, (2.5 * 3600 * 1000).toLong(), 40 * 60000L, R.string.inhaled_afrezza, R.string.inhaled_afrezza_comment, isInhaled = true);

val iCfg: ICfg
get() = ICfg(this.name, insulinEndTime, insulinPeakTime, 1.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface HardLimits {
val LIMIT_TEMP_TARGET_BG = doubleArrayOf(72.0, 200.0)
val MIN_DIA = doubleArrayOf(5.0, 5.0, 5.0, 5.0, 5.0)
val MAX_DIA = doubleArrayOf(9.0, 9.0, 9.0, 9.0, 10.0)
val MIN_DIA_INHALED = doubleArrayOf(1.5, 1.5, 1.5, 1.5, 1.5) // Inhaled insulin (e.g. Afrezza) has shorter DIA
val MAX_DIA_INHALED = doubleArrayOf(4.0, 4.0, 4.0, 4.0, 4.0)
const val MIN_PEAK = 35 // mgdl
const val MAX_PEAK = 120 // mgdl
val MIN_IC = doubleArrayOf(2.0, 2.0, 2.0, 2.0, 0.3)
Expand All @@ -41,6 +43,8 @@ interface HardLimits {
fun maxBasal(): Double
fun minDia(): Double
fun maxDia(): Double
fun minDiaInhaled(): Double
fun maxDiaInhaled(): Double
fun minPeak(): Int
fun maxPeak(): Int
fun minIC(): Double
Expand All @@ -57,4 +61,4 @@ interface HardLimits {
fun ageEntryValues(): Array<CharSequence>

enum class AgeType { CHILD, TEENAGE, ADULT, RESISTANT_ADULT, PREGNANT }
}
}
2 changes: 2 additions & 0 deletions core/interfaces/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
<string name="ultra_rapid_oref">Fiasp</string>
<string name="fast_acting_insulin_comment">Novorapid, Novolog, Humalog</string>
<string name="ultra_fast_acting_insulin_comment">Fiasp</string>
<string name="inhaled_afrezza">Afrezza (Inhaled)</string>
<string name="inhaled_afrezza_comment">Technosphere inhaled insulin — peak ~40 min, duration ~2.5 h</string>
<string name="insulin_peak_time">Peak Time [min]</string>

<!-- Concentration -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum class ElementType(
BOLUS_WIZARD(category = ElementCategory.TREATMENT, searchable = true, protection = ProtectionCheck.Protection.BOLUS),
QUICK_WIZARD(protection = ProtectionCheck.Protection.BOLUS),
TREATMENT(category = ElementCategory.TREATMENT, searchable = true, protection = ProtectionCheck.Protection.BOLUS),
AFREZZA(category = ElementCategory.TREATMENT, searchable = true, protection = ProtectionCheck.Protection.BOLUS),

// CGM
CGM_XDRIP(category = ElementCategory.CGM, searchable = true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
fun ElementType.color(): Color = when (this) {
ElementType.INSULIN,
ElementType.TREATMENT,
ElementType.AFREZZA,
ElementType.FILL -> AapsTheme.elementColors.insulin

ElementType.CARBS -> AapsTheme.elementColors.carbs
Expand Down Expand Up @@ -114,8 +115,9 @@
ElementType.EXIT -> AapsTheme.elementColors.navigation
}

fun ElementType.icon(): ImageVector = when (this) {

Check warning on line 118 in core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the number of when branches from 45 to at most 30.

See more on https://sonarcloud.io/project/issues?id=nightscout_AndroidAPS&issues=AZ6KguWisynw1OJ1GdZv&open=AZ6KguWisynw1OJ1GdZv&pullRequest=4877
ElementType.INSULIN -> IcBolus
ElementType.AFREZZA -> IcBolus
ElementType.CARBS -> IcCarbs
ElementType.BOLUS_WIZARD -> IcCalculator
ElementType.QUICK_WIZARD,
Expand Down Expand Up @@ -169,8 +171,9 @@
ElementType.EXIT -> Icons.AutoMirrored.Filled.ExitToApp
}

fun ElementType.labelResId(): Int = when (this) {

Check warning on line 174 in core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the number of when branches from 48 to at most 30.

See more on https://sonarcloud.io/project/issues?id=nightscout_AndroidAPS&issues=AZ6KguWisynw1OJ1GdZw&open=AZ6KguWisynw1OJ1GdZw&pullRequest=4877
ElementType.INSULIN -> R.string.overview_insulin_label
ElementType.AFREZZA -> R.string.overview_afrezza_label
ElementType.CARBS -> R.string.carbs
ElementType.BOLUS_WIZARD -> R.string.boluswizard
ElementType.QUICK_WIZARD -> 0 // dynamic label
Expand Down Expand Up @@ -219,8 +222,9 @@
ElementType.EXIT -> R.string.nav_exit
}

fun ElementType.descriptionResId(): Int = when (this) {

Check warning on line 225 in core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the number of when branches from 37 to at most 30.

See more on https://sonarcloud.io/project/issues?id=nightscout_AndroidAPS&issues=AZ6KguWisynw1OJ1GdZx&open=AZ6KguWisynw1OJ1GdZx&pullRequest=4877
ElementType.INSULIN -> R.string.treatment_insulin_desc
ElementType.AFREZZA -> R.string.treatment_afrezza_desc
ElementType.CARBS -> R.string.treatment_carbs_desc
ElementType.BOLUS_WIZARD -> R.string.treatment_calculator_desc
ElementType.TREATMENT -> R.string.treatment_desc
Expand Down
4 changes: 4 additions & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
<string name="select_site_location">Select site location</string>
<string name="site_location">Site location</string>
<string name="overview_insulin_label">Insulin</string>
<string name="overview_afrezza_label">Afrezza</string>
<string name="stoptemptarget">Stop temp target</string>
<string name="closedloop">Closed Loop</string>
<string name="openloop">Open Loop</string>
Expand Down Expand Up @@ -403,6 +404,7 @@
<string name="boluswizard">Bolus wizard</string>
<string name="treatment_desc">Record insulin and carbs together</string>
<string name="treatment_insulin_desc">Deliver bolus manually</string>
<string name="treatment_afrezza_desc">Log inhaled Afrezza dose</string>
<string name="treatment_carbs_desc">Record carbs without insulin</string>
<string name="treatment_calculator_desc">Calculate bolus</string>
<string name="insulin_management">Insulin</string>
Expand Down Expand Up @@ -746,6 +748,8 @@
<string name="wizard_explain_tt">TempT: %1$s</string>
<string name="wizard_explain_tt_to">%1$s to %2$s</string>
<string name="wizard_pump_not_available">No pump available!</string>
<string name="quickwizard">QuickWizard</string>
<string name="afrezza_not_configured">Add Afrezza insulin in Insulin Management first</string>
<string name="quickwizard_managemnt">QuickWizard</string>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ data class UserEntry(
enum class Sources {
TreatmentDialog,
InsulinDialog,
AfrezzaDialog,
CarbDialog,
WizardDialog,
QuickWizard,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ fun UserEntry.Sources.fromDb(): Sources =
when (this) {
UserEntry.Sources.TreatmentDialog -> Sources.TreatmentDialog
UserEntry.Sources.InsulinDialog -> Sources.InsulinDialog
UserEntry.Sources.AfrezzaDialog -> Sources.AfrezzaDialog
UserEntry.Sources.CarbDialog -> Sources.CarbDialog
UserEntry.Sources.WizardDialog -> Sources.WizardDialog
UserEntry.Sources.QuickWizard -> Sources.QuickWizard
Expand Down Expand Up @@ -94,6 +95,7 @@ fun Sources.toDb(): UserEntry.Sources =
when (this) {
Sources.TreatmentDialog -> UserEntry.Sources.TreatmentDialog
Sources.InsulinDialog -> UserEntry.Sources.InsulinDialog
Sources.AfrezzaDialog -> UserEntry.Sources.AfrezzaDialog
Sources.CarbDialog -> UserEntry.Sources.CarbDialog
Sources.WizardDialog -> UserEntry.Sources.WizardDialog
Sources.QuickWizard -> UserEntry.Sources.QuickWizard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class InsulinImpl @Inject constructor(
InsulinType.OREF_RAPID_ACTING,
InsulinType.OREF_ULTRA_RAPID_ACTING,
InsulinType.OREF_LYUMJEV,
InsulinType.OREF_FREE_PEAK
InsulinType.OREF_FREE_PEAK,
InsulinType.OREF_INHALED_AFREZZA
)

override fun concentrationList(): List<ConcentrationType> = listOf(
Expand Down
Loading