소스 검색

Merge pull request #339 from MikePlante1/maxIOB-maxCOB

Bolus Calculator Improvements
Deniz Cengiz 1 년 전
부모
커밋
782a6108ff

+ 14 - 0
Model/Helper/Determination+helper.swift

@@ -19,6 +19,20 @@ extension OrefDetermination {
     var reasonConclusion: String {
     var reasonConclusion: String {
         reason?.components(separatedBy: "; ").last ?? ""
         reason?.components(separatedBy: "; ").last ?? ""
     }
     }
+
+    var minPredBGFromReason: Decimal? {
+        // Find the part that contains "minPredBG"
+        if let minPredBGPart = reasonParts.first(where: { $0.contains("minPredBG") }) {
+            // Extract the number after "minPredBG"
+            let components = minPredBGPart.components(separatedBy: "minPredBG ")
+            if let valueComponent = components.dropFirst().first {
+                // Get everything after "minPredBG " and convert to Decimal
+                let valueString = valueComponent.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.-").inverted)
+                return Decimal(string: valueString)
+            }
+        }
+        return nil
+    }
 }
 }
 
 
 extension NSPredicate {
 extension NSPredicate {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 178 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 2 - 2
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -48,7 +48,7 @@ struct DecimalPickerSettings {
     var maxCarbs = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxCarbs = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxFat = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxFat = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxProtein = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxProtein = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
-    var overrideFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.5, max: 1.5, type: PickerSetting.PickerSettingType.factor)
+    var overrideFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.05, max: 1.5, type: PickerSetting.PickerSettingType.factor)
     var fattyMealFactor = PickerSetting(value: 0.7, step: 0.05, min: 0.05, max: 1, type: PickerSetting.PickerSettingType.factor)
     var fattyMealFactor = PickerSetting(value: 0.7, step: 0.05, min: 0.05, max: 1, type: PickerSetting.PickerSettingType.factor)
     var sweetMealFactor = PickerSetting(value: 1, step: 0.05, min: 0.05, max: 2, type: PickerSetting.PickerSettingType.factor)
     var sweetMealFactor = PickerSetting(value: 1, step: 0.05, min: 0.05, max: 2, type: PickerSetting.PickerSettingType.factor)
     var maxIOB = PickerSetting(value: 0, step: 1, min: 0, max: 20, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxIOB = PickerSetting(value: 0, step: 1, min: 0, max: 20, type: PickerSetting.PickerSettingType.insulinUnit)
@@ -136,7 +136,7 @@ struct DecimalPickerSettings {
     var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, type: PickerSetting.PickerSettingType.hour)
-    var maxBolus = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxBolus = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxBasal = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxBasal = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
 }
 }
 
 

+ 5 - 0
Trio/Sources/Models/TrioSettings.swift

@@ -66,6 +66,7 @@ struct TrioSettings: JSON, Equatable {
     var sweetMeals: Bool = false
     var sweetMeals: Bool = false
     var sweetMealFactor: Decimal = 1
     var sweetMealFactor: Decimal = 1
     var displayPresets: Bool = true
     var displayPresets: Bool = true
+    var confirmBolus: Bool = false
     var useLiveActivity: Bool = false
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
     var lockScreenView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var bolusShortcut: BolusShortcutLimit = .notAllowed
@@ -284,6 +285,10 @@ extension TrioSettings: Decodable {
             settings.displayPresets = displayPresets
             settings.displayPresets = displayPresets
         }
         }
 
 
+        if let confirmBolus = try? container.decode(Bool.self, forKey: .confirmBolus) {
+            settings.confirmBolus = confirmBolus
+        }
+
         if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
         if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
             settings.useLiveActivity = useLiveActivity
             settings.useLiveActivity = useLiveActivity
         }
         }

+ 5 - 18
Trio/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -9,31 +9,18 @@ extension BolusCalculatorConfig {
         @Published var sweetMeals: Bool = false
         @Published var sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var sweetMealFactor: Decimal = 0
         @Published var displayPresets: Bool = true
         @Published var displayPresets: Bool = true
+        @Published var confirmBolusWhenVeryLowGlucose: Bool = false
 
 
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
 
 
-            subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
-                let value = max(min($0, 1.2), 0.1)
-                overrideFactor = value
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.overrideFactor, on: $overrideFactor) { overrideFactor = $0 }
             subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
             subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
             subscribeSetting(\.displayPresets, on: $displayPresets) { displayPresets = $0 }
             subscribeSetting(\.displayPresets, on: $displayPresets) { displayPresets = $0 }
-            subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
-                let value = max(min($0, 1.2), 0.1)
-                fattyMealFactor = value
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor) { fattyMealFactor = $0 }
             subscribeSetting(\.sweetMeals, on: $sweetMeals) { sweetMeals = $0 }
             subscribeSetting(\.sweetMeals, on: $sweetMeals) { sweetMeals = $0 }
-            subscribeSetting(\.sweetMealFactor, on: $sweetMealFactor, initial: {
-                let value = max(min($0, 5), 1)
-                sweetMealFactor = value
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.sweetMealFactor, on: $sweetMealFactor) { sweetMealFactor = $0 }
+            subscribeSetting(\.confirmBolus, on: $confirmBolusWhenVeryLowGlucose) { confirmBolusWhenVeryLowGlucose = $0 }
         }
         }
     }
     }
 }
 }

+ 33 - 2
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -113,7 +113,7 @@ extension BolusCalculatorConfig {
                             "When \"Fatty Meal\" is selected in the bolus calculator, the recommended bolus will be multiplied by the \"Fatty Meal Bolus Percentage\" as well as the \"Recommended Bolus Percentage\"."
                             "When \"Fatty Meal\" is selected in the bolus calculator, the recommended bolus will be multiplied by the \"Fatty Meal Bolus Percentage\" as well as the \"Recommended Bolus Percentage\"."
                         )
                         )
                         Text(
                         Text(
-                            "If you have a \"Recommended Bolus Percentage\" of 80%, and a \"Fatty Meal Bolus Percentage\" of 70%, your recommended bolus will be multiplied by: (80 × 70) ÷ 100 = 56%."
+                            "If you have a \"Recommended Bolus Percentage\" of 80%, and a \"Fatty Meal Bolus Percentage\" of 70%, your recommended bolus will be multiplied by: (80 × 70) / 100 = 56%."
                         )
                         )
                         Text("This could be useful for slow absorbing meals like pizza.")
                         Text("This could be useful for slow absorbing meals like pizza.")
                     }
                     }
@@ -147,11 +147,42 @@ extension BolusCalculatorConfig {
                             "When \"Super Bolus\" is selected in the bolus calculator, your current basal rate multiplied by \"Super Bolus Percentage\" will be added to your bolus recommendation."
                             "When \"Super Bolus\" is selected in the bolus calculator, your current basal rate multiplied by \"Super Bolus Percentage\" will be added to your bolus recommendation."
                         )
                         )
                         Text(
                         Text(
-                            "If your current basal rate is 0.8 U/hr and \"Super Bolus Percentage\" is set to 200%: 0.8 × (200 ÷ 100) = 1.6 units will be added to your bolus recommendation."
+                            "If your current basal rate is 0.8 U/hr and \"Super Bolus Percentage\" is set to 200%: 0.8 × (200 / 100) = 1.6 units will be added to your bolus recommendation."
                         )
                         )
                         Text("This could be useful for fast absorbing meals like sugary cereal.")
                         Text("This could be useful for fast absorbing meals like sugary cereal.")
                     }
                     }
                 )
                 )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.confirmBolusWhenVeryLowGlucose,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "Very Low Glucose Bolus Warning")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: String(localized: "Very Low Glucose Warning"),
+                    miniHint: String(
+                        localized: "Warning when bolusing with a very low or forecasted very low glucose."
+                    ),
+                    verboseHint: VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: OFF").bold()
+                        Text(
+                            "Triggers a confirmation dialog if you attempt to bolus when glucose is < \(state.units == .mgdL ? 54.description : 54.formattedAsMmolL) \(state.units.rawValue)."
+                        )
+                        Text(
+                            "Also triggered when the lowest forecasted glucose (minPredBG) is < \(state.units == .mgdL ? 54.description : 54.formattedAsMmolL) \(state.units.rawValue)."
+                        )
+                        Text(
+                            "Note: The forecast used for this warning does not include carbs or insulin that have not yet been logged."
+                        )
+                    }
+                )
             }
             }
             .listSectionSpacing(sectionSpacing)
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {
             .sheet(isPresented: $shouldDisplayHint) {

+ 1 - 1
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -256,7 +256,7 @@ extension DynamicSettings {
                             Text(
                             Text(
                                 "Enabling Adjust Basal replaces the standard Autosens Ratio calculation with its own Autosens Ratio calculated as such:"
                                 "Enabling Adjust Basal replaces the standard Autosens Ratio calculation with its own Autosens Ratio calculated as such:"
                             )
                             )
-                            Text("Autosens Ratio =\n(Weighted Average of TDD) ÷ (10-day Average of TDD)")
+                            Text("Autosens Ratio =\n(Weighted Average of TDD) / (10-day Average of TDD)")
                             Text("New Basal Profile =\n(Current Basal Profile) × (Autosens Ratio)")
                             Text("New Basal Profile =\n(Current Basal Profile) × (Autosens Ratio)")
                         },
                         },
                         headerText: String(localized: "Dynamic-dependent Features")
                         headerText: String(localized: "Dynamic-dependent Features")

+ 2 - 2
Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift

@@ -262,7 +262,7 @@ extension SMBSettings {
                             Text(
                             Text(
                                 "𝒳 = Max SMB Basal Minutes"
                                 "𝒳 = Max SMB Basal Minutes"
                             )
                             )
-                            Text("(𝒳 ÷ 60) × current basal rate")
+                            Text("(𝒳 / 60) × current basal rate")
                         }
                         }
 
 
                         VStack(alignment: .leading, spacing: 10) {
                         VStack(alignment: .leading, spacing: 10) {
@@ -308,7 +308,7 @@ extension SMBSettings {
                             Text(
                             Text(
                                 "𝒳 = Max UAM SMB Basal Minutes"
                                 "𝒳 = Max UAM SMB Basal Minutes"
                             )
                             )
-                            Text("(𝒳 ÷ 60) × current basal rate")
+                            Text("(𝒳 / 60) × current basal rate")
                         }
                         }
                         VStack(alignment: .leading, spacing: 10) {
                         VStack(alignment: .leading, spacing: 10) {
                             Text(
                             Text(

+ 6 - 0
Trio/Sources/Modules/Treatments/TreatmentsProvider.swift

@@ -33,5 +33,11 @@ extension Treatments {
                     sensitivities: []
                     sensitivities: []
                 )
                 )
         }
         }
+
+        func getPreferences() async -> Preferences {
+            await storage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self)
+                ?? Preferences(from: OpenAPS.defaults(for: OpenAPS.Settings.preferences))
+                ?? Preferences(maxIOB: 0, maxCOB: 120)
+        }
     }
     }
 }
 }

+ 36 - 3
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -31,6 +31,8 @@ extension Treatments {
         var threshold: Decimal = 0
         var threshold: Decimal = 0
         var maxBolus: Decimal = 0
         var maxBolus: Decimal = 0
         var maxExternal: Decimal { maxBolus * 3 }
         var maxExternal: Decimal { maxBolus * 3 }
+        var maxIOB: Decimal = 0
+        var maxCOB: Decimal = 0
         var errorString: Decimal = 0
         var errorString: Decimal = 0
         var evBG: Decimal = 0
         var evBG: Decimal = 0
         var insulin: Decimal = 0
         var insulin: Decimal = 0
@@ -40,6 +42,7 @@ extension Treatments {
         var minDelta: Decimal = 0
         var minDelta: Decimal = 0
         var expectedDelta: Decimal = 0
         var expectedDelta: Decimal = 0
         var minPredBG: Decimal = 0
         var minPredBG: Decimal = 0
+        var lastLoopDate: Date?
         var isAwaitingDeterminationResult: Bool = false
         var isAwaitingDeterminationResult: Bool = false
         var carbRatio: Decimal = 0
         var carbRatio: Decimal = 0
 
 
@@ -58,6 +61,7 @@ extension Treatments {
         var wholeCobInsulin: Decimal = 0
         var wholeCobInsulin: Decimal = 0
         var iobInsulinReduction: Decimal = 0
         var iobInsulinReduction: Decimal = 0
         var wholeCalc: Decimal = 0
         var wholeCalc: Decimal = 0
+        var factoredInsulin: Decimal = 0
         var insulinCalculated: Decimal = 0
         var insulinCalculated: Decimal = 0
         var fraction: Decimal = 0
         var fraction: Decimal = 0
         var basal: Decimal = 0
         var basal: Decimal = 0
@@ -65,6 +69,7 @@ extension Treatments {
         var fattyMealFactor: Decimal = 0
         var fattyMealFactor: Decimal = 0
         var useFattyMealCorrectionFactor: Bool = false
         var useFattyMealCorrectionFactor: Bool = false
         var displayPresets: Bool = true
         var displayPresets: Bool = true
+        var confirmBolus: Bool = false
 
 
         var currentBasal: Decimal = 0
         var currentBasal: Decimal = 0
         var currentCarbRatio: Decimal = 0
         var currentCarbRatio: Decimal = 0
@@ -244,6 +249,13 @@ extension Treatments {
                         self.maxBolus = getMaxBolus
                         self.maxBolus = getMaxBolus
                     }
                     }
                 }
                 }
+                group.addTask {
+                    let getPreferences = await self.provider.getPreferences()
+                    await MainActor.run {
+                        self.maxIOB = getPreferences.maxIOB
+                        self.maxCOB = getPreferences.maxCOB
+                    }
+                }
             }
             }
         }
         }
 
 
@@ -274,6 +286,7 @@ extension Treatments {
             sweetMeals = settings.settings.sweetMeals
             sweetMeals = settings.settings.sweetMeals
             sweetMealFactor = settings.settings.sweetMealFactor
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
             displayPresets = settings.settings.displayPresets
+            confirmBolus = settings.settings.confirmBolus
             forecastDisplayType = settings.settings.forecastDisplayType
             forecastDisplayType = settings.settings.forecastDisplayType
             lowGlucose = settingsManager.settings.low
             lowGlucose = settingsManager.settings.low
             highGlucose = settingsManager.settings.high
             highGlucose = settingsManager.settings.high
@@ -373,6 +386,7 @@ extension Treatments {
                 iobInsulinReduction = result.iobInsulinReduction
                 iobInsulinReduction = result.iobInsulinReduction
                 superBolusInsulin = result.superBolusInsulin
                 superBolusInsulin = result.superBolusInsulin
                 wholeCalc = result.wholeCalc
                 wholeCalc = result.wholeCalc
+                factoredInsulin = result.factoredInsulin
                 fifteenMinInsulin = result.fifteenMinutesInsulin
                 fifteenMinInsulin = result.fifteenMinutesInsulin
             }
             }
 
 
@@ -673,11 +687,28 @@ extension Treatments.StateModel {
     }
     }
 
 
     @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
     @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+        // Store all objects for the forecast graph
         glucoseFromPersistence = objects
         glucoseFromPersistence = objects
 
 
-        let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
-        let thirdLastGlucose = glucoseFromPersistence.dropFirst(2).first?.glucose ?? 0
-        let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+        // Always use the most recent reading for current glucose
+        let lastGlucose = objects.first?.glucose ?? 0
+
+        // Filter for readings less than 20 minutes old
+        let twentyMinutesAgo = Date().addingTimeInterval(-20 * 60)
+        let recentObjects = objects.filter {
+            guard let date = $0.date else { return false }
+            return date > twentyMinutesAgo
+        }
+
+        // Calculate delta using newest and oldest readings within 20-minute window
+        let delta: Decimal
+        if let newestInWindow = recentObjects.first?.glucose, let oldestInWindow = recentObjects.last?.glucose {
+            // Newest is at index 0, oldest is at the last index
+            delta = Decimal(newestInWindow) - Decimal(oldestInWindow)
+        } else {
+            // Not enough data points in the window
+            delta = 0
+        }
 
 
         currentBG = Decimal(lastGlucose)
         currentBG = Decimal(lastGlucose)
         deltaBG = delta
         deltaBG = delta
@@ -765,6 +796,8 @@ extension Treatments.StateModel {
             // setup vars for bolus calculation
             // setup vars for bolus calculation
             insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
             insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
             evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
             evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
+            minPredBG = (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal
+            lastLoopDate = apsManager.lastLoopDate as Date?
             insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
             insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
             target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
             target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
             isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
             isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 870 - 379
Trio/Sources/Modules/Treatments/View/PopupView.swift


+ 67 - 19
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -391,8 +391,6 @@ extension Treatments {
             }
             }
             .sheet(isPresented: $state.showInfo) {
             .sheet(isPresented: $state.showInfo) {
                 PopupView(state: state)
                 PopupView(state: state)
-                    .presentationDetents([.fraction(0.9), .large])
-                    .presentationDragIndicator(.visible)
             }
             }
             .sheet(isPresented: $showPresetSheet, onDismiss: {
             .sheet(isPresented: $showPresetSheet, onDismiss: {
                 showPresetSheet = false
                 showPresetSheet = false
@@ -421,6 +419,28 @@ extension Treatments {
             }
             }
         }
         }
 
 
+        @State private var showConfirmDialogForBolusing = false
+
+        private var bolusWarning: (shouldConfirm: Bool, warningMessage: String, color: Color) {
+            let isGlucoseVeryLow = state.currentBG < 54
+            let isForecastVeryLow = state.minPredBG < 54
+
+            // Only warn when enacting a bolus via pump
+            guard !state.externalInsulin, state.amount > 0 else {
+                return (false, "", .primary)
+            }
+
+            let warningMessage = isGlucoseVeryLow ? String(localized: "Glucose is very low.") :
+                isForecastVeryLow ? String(localized: "Glucose forecast is very low.") :
+                ""
+
+            let warningColor: Color = isGlucoseVeryLow ? .red : colorScheme == .dark ? .orange : .accentColor
+
+            let shouldConfirm = state.confirmBolus && (isGlucoseVeryLow || isForecastVeryLow)
+
+            return (shouldConfirm, warningMessage, warningColor)
+        }
+
         var treatmentButton: some View {
         var treatmentButton: some View {
             var treatmentButtonBackground = Color(.systemBlue)
             var treatmentButtonBackground = Color(.systemBlue)
             if limitExceeded {
             if limitExceeded {
@@ -429,26 +449,54 @@ extension Treatments {
                 treatmentButtonBackground = Color(.systemGray)
                 treatmentButtonBackground = Color(.systemGray)
             }
             }
 
 
-            return Button {
-                state.invokeTreatmentsTask()
-            } label: {
-                HStack {
-                    if state.isBolusInProgress && state
-                        .amount > 0 && !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
-                    {
-                        ProgressView()
+            return Section {
+                Button {
+                    if bolusWarning.shouldConfirm {
+                        showConfirmDialogForBolusing = true
+                    } else {
+                        state.invokeTreatmentsTask()
+                    }
+                } label: {
+                    HStack {
+                        if state.isBolusInProgress && state.amount > 0 &&
+                            !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
+                        {
+                            ProgressView()
+                        }
+                        taskButtonLabel
                     }
                     }
-                    taskButtonLabel
+                    .font(.headline)
+                    .foregroundStyle(Color.white)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .frame(height: 35)
+                }
+                .disabled(disableTaskButton)
+                .listRowBackground(treatmentButtonBackground)
+                .shadow(radius: 3)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .confirmationDialog(
+                    bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
+                    isPresented: $showConfirmDialogForBolusing,
+                    titleVisibility: .visible
+                ) {
+                    Button("Cancel", role: .cancel) {}
+                    Button(
+                        bolusWarning.warningMessage.isEmpty ? "Enact Bolus" : "Ignore Warning and Enact Bolus",
+                        role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                    ) {
+                        state.invokeTreatmentsTask()
+                    }
+                }
+            } header: {
+                if !bolusWarning.warningMessage.isEmpty {
+                    Text(bolusWarning.warningMessage)
+                        .textCase(nil)
+                        .font(.subheadline)
+                        .foregroundColor(bolusWarning.color)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .padding(.top, -22)
                 }
                 }
-                .font(.headline)
-                .foregroundStyle(Color.white)
-                .frame(maxWidth: .infinity, alignment: .center)
-                .frame(height: 35)
             }
             }
-            .disabled(disableTaskButton)
-            .listRowBackground(treatmentButtonBackground)
-            .shadow(radius: 3)
-            .clipShape(RoundedRectangle(cornerRadius: 8))
         }
         }
 
 
         private var taskButtonLabel: some View {
         private var taskButtonLabel: some View {

+ 87 - 22
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -30,6 +30,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     private struct BolusCalculatorVariables {
     private struct BolusCalculatorVariables {
         var insulinRequired: Decimal
         var insulinRequired: Decimal
         var evBG: Decimal
         var evBG: Decimal
+        var minPredBG: Decimal
+        var lastLoopDate: Date?
         var insulin: Decimal
         var insulin: Decimal
         var target: Decimal
         var target: Decimal
         var isf: Decimal
         var isf: Decimal
@@ -170,6 +172,14 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             )
             )
     }
     }
 
 
+    /// Retrieves Preferences from storage
+    /// - Returns: Preferences object containing maxIOB and maxCOB
+    private func getPreferences() async -> Preferences {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self)
+            ?? Preferences(from: OpenAPS.defaults(for: OpenAPS.Settings.preferences))
+            ?? Preferences(maxIOB: 0, maxCOB: 120)
+    }
+
     /// Fetches recent glucose readings from CoreData
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
@@ -194,9 +204,27 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     /// - Parameter objects: Array of GlucoseStored objects
     /// - Parameter objects: Array of GlucoseStored objects
     /// - Returns: GlucoseVariables containing current blood glucose and delta
     /// - Returns: GlucoseVariables containing current blood glucose and delta
     private func updateGlucoseVariables(with objects: [GlucoseStored]) -> GlucoseVariables {
     private func updateGlucoseVariables(with objects: [GlucoseStored]) -> GlucoseVariables {
+        // Always use the most recent reading for current glucose regardless of time
         let lastGlucose = objects.first?.glucose ?? 0
         let lastGlucose = objects.first?.glucose ?? 0
-        let thirdLastGlucose = objects.dropFirst(2).first?.glucose ?? 0
-        let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+
+        // Filter for readings less than 20 minutes old
+        let twentyMinutesAgo = Date().addingTimeInterval(-20 * 60)
+        let recentObjects = objects.filter {
+            guard let date = $0.date else { return false }
+            return date > twentyMinutesAgo
+        }
+
+        // Calculate delta using newest and oldest readings within 20-minute window
+        let delta: Decimal
+        if recentObjects.count >= 2 {
+            // Newest is at index 0, oldest is at the last index
+            let newestInWindow = recentObjects.first?.glucose ?? 0
+            let oldestInWindow = recentObjects.last?.glucose ?? 0
+            delta = Decimal(newestInWindow) - Decimal(oldestInWindow)
+        } else {
+            // Not enough data points in the window
+            delta = 0
+        }
 
 
         return GlucoseVariables(currentBG: Decimal(lastGlucose), deltaBG: delta)
         return GlucoseVariables(currentBG: Decimal(lastGlucose), deltaBG: delta)
     }
     }
@@ -220,6 +248,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             return BolusCalculatorVariables(
             return BolusCalculatorVariables(
                 insulinRequired: 0,
                 insulinRequired: 0,
                 evBG: 0,
                 evBG: 0,
+                minPredBG: 0,
+                lastLoopDate: nil,
                 insulin: 0,
                 insulin: 0,
                 target: currentBGTarget,
                 target: currentBGTarget,
                 isf: currentISF,
                 isf: currentISF,
@@ -234,6 +264,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         return BolusCalculatorVariables(
         return BolusCalculatorVariables(
             insulinRequired: (mostRecentDetermination.insulinReq ?? 0) as Decimal,
             insulinRequired: (mostRecentDetermination.insulinReq ?? 0) as Decimal,
             evBG: (mostRecentDetermination.eventualBG ?? 0) as Decimal,
             evBG: (mostRecentDetermination.eventualBG ?? 0) as Decimal,
+            minPredBG: (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal,
+            lastLoopDate: apsManager.lastLoopDate as Date?,
             insulin: (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal,
             insulin: (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal,
             target: (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal,
             target: (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal,
             isf: (mostRecentDetermination.insulinSensitivity ?? NSDecimalNumber(decimal: currentISF)) as Decimal,
             isf: (mostRecentDetermination.insulinSensitivity ?? NSDecimalNumber(decimal: currentISF)) as Decimal,
@@ -263,6 +295,12 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
             let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
             let currentISF = await getCurrentSettingValue(for: .isf)
             let currentISF = await getCurrentSettingValue(for: .isf)
 
 
+            // Get max IOB and max COB
+
+            let preferences = await getPreferences()
+            let maxIOB = preferences.maxIOB
+            let maxCOB = preferences.maxCOB
+
             // Fetch glucose data
             // Fetch glucose data
             let glucoseIds = try await fetchGlucose()
             let glucoseIds = try await fetchGlucose()
             let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared.getNSManagedObject(
             let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared.getNSManagedObject(
@@ -306,7 +344,10 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 sweetMealFactor: settings.sweetMealFactor,
                 sweetMealFactor: settings.sweetMealFactor,
                 basal: bolusVars.basal,
                 basal: bolusVars.basal,
                 fraction: settings.fraction,
                 fraction: settings.fraction,
-                maxBolus: maxBolus
+                maxBolus: maxBolus,
+                maxIOB: maxIOB,
+                maxCOB: maxCOB,
+                minPredBG: bolusVars.minPredBG
             )
             )
         } catch {
         } catch {
             debug(
             debug(
@@ -324,14 +365,15 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     func calculateInsulin(input: CalculationInput) async -> CalculationResult {
     func calculateInsulin(input: CalculationInput) async -> CalculationResult {
         // insulin needed for the current blood glucose
         // insulin needed for the current blood glucose
         let targetDifference = input.currentBG - input.target
         let targetDifference = input.currentBG - input.target
-        let targetDifferenceInsulin = apsManager.roundBolus(amount: targetDifference / input.isf)
+
+        let targetDifferenceInsulin = targetDifference / input.isf
 
 
         // more or less insulin because of bg trend in the last 15 minutes
         // more or less insulin because of bg trend in the last 15 minutes
-        let fifteenMinutesInsulin = apsManager.roundBolus(amount: input.deltaBG / input.isf)
+        let fifteenMinutesInsulin = input.deltaBG / input.isf
 
 
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
-        let wholeCob = Decimal(input.cob) + input.carbs
-        let wholeCobInsulin = apsManager.roundBolus(amount: wholeCob / input.carbRatio)
+        let wholeCob = min(Decimal(input.cob) + input.carbs, input.maxCOB)
+        let wholeCobInsulin = wholeCob / input.carbRatio
 
 
         // determine how much the calculator reduces/ increases the bolus because of IOB
         // determine how much the calculator reduces/ increases the bolus because of IOB
         let iobInsulinReduction = (-1) * input.iob
         let iobInsulinReduction = (-1) * input.iob
@@ -352,29 +394,47 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         }
         }
 
 
         // apply custom factor at the end of the calculations
         // apply custom factor at the end of the calculations
-        let result = wholeCalc * input.fraction
-
         // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
         // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
-        var insulinCalculated: Decimal
+        var factoredInsulin = wholeCalc
+
+        // Apply Recommended Bolus Percentage (input.fraction) and if selected apply Fatty Meal Bolus Percentage (input.fattyMealFactor)
+        // If factoredInsulin is negative, though, don't apply either
+        if factoredInsulin > 0 {
+            factoredInsulin *= input.fraction
+
+            if input.useFattyMealCorrectionFactor {
+                factoredInsulin *= input.fattyMealFactor
+            }
+        }
+
+        // Calculate and add super bolus insulin if enabled
         var superBolusInsulin: Decimal = 0
         var superBolusInsulin: Decimal = 0
-        if input.useFattyMealCorrectionFactor {
-            insulinCalculated = result * input.fattyMealFactor
-        } else if input.useSuperBolus {
+        if input.useSuperBolus {
             superBolusInsulin = input.sweetMealFactor * input.basal
             superBolusInsulin = input.sweetMealFactor * input.basal
-            insulinCalculated = result + superBolusInsulin
-        } else {
-            insulinCalculated = result
+            factoredInsulin += superBolusInsulin
         }
         }
 
 
-        // display no negative insulinCalculated
-        insulinCalculated = max(insulinCalculated, 0)
-        insulinCalculated = min(insulinCalculated, input.maxBolus)
+        // the final result for recommended insulin amount
+        var insulinCalculated: Decimal
+        let isLoopStale = Date().timeIntervalSince(apsManager.lastLoopDate) > 15 * 60
 
 
-        // round calculated recommendation to allowed bolus increment
-        insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+        // don't recommend insulin when current glucose or minPredBG is < 54 or last sucessful loop was over 15 minutes ago
+        if input.currentBG < 54 || input.minPredBG < 54 || isLoopStale {
+            insulinCalculated = 0
+        } else {
+            // no negative insulinCalculated
+            insulinCalculated = max(factoredInsulin, 0)
+            // don't exceed maxBolus
+            insulinCalculated = min(insulinCalculated, input.maxBolus)
+            // don't exceed maxIOB
+            insulinCalculated = min(insulinCalculated, input.maxIOB - input.iob)
+            // round calculated recommendation to allowed bolus increment
+            insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+        }
 
 
         return CalculationResult(
         return CalculationResult(
             insulinCalculated: insulinCalculated,
             insulinCalculated: insulinCalculated,
+            factoredInsulin: factoredInsulin,
             wholeCalc: wholeCalc,
             wholeCalc: wholeCalc,
             correctionInsulin: targetDifferenceInsulin,
             correctionInsulin: targetDifferenceInsulin,
             iobInsulinReduction: iobInsulinReduction,
             iobInsulinReduction: iobInsulinReduction,
@@ -414,6 +474,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             // Return safe default values
             // Return safe default values
             return CalculationResult(
             return CalculationResult(
                 insulinCalculated: 0,
                 insulinCalculated: 0,
+                factoredInsulin: 0,
                 wholeCalc: 0,
                 wholeCalc: 0,
                 correctionInsulin: 0,
                 correctionInsulin: 0,
                 iobInsulinReduction: 0,
                 iobInsulinReduction: 0,
@@ -445,11 +506,15 @@ struct CalculationInput: Sendable {
     let basal: Decimal // Current basal rate
     let basal: Decimal // Current basal rate
     let fraction: Decimal // General correction factor
     let fraction: Decimal // General correction factor
     let maxBolus: Decimal // Maximum allowed bolus
     let maxBolus: Decimal // Maximum allowed bolus
+    let maxIOB: Decimal // Maximum allowed IOB to be used for rec. bolus calculation
+    let maxCOB: Decimal // Maximum allowed COB to be used for rec. bolus calculation
+    let minPredBG: Decimal // Minimum Predicted Glucose determined by Oref
 }
 }
 
 
 /// Results of the bolus calculation
 /// Results of the bolus calculation
 struct CalculationResult: Sendable {
 struct CalculationResult: Sendable {
-    let insulinCalculated: Decimal // Final calculated insulin amount
+    let insulinCalculated: Decimal // Final calculated insulin amount which respects limits
+    let factoredInsulin: Decimal // Total calculation after adjustments
     let wholeCalc: Decimal // Total calculation before adjustments
     let wholeCalc: Decimal // Total calculation before adjustments
     let correctionInsulin: Decimal // Insulin for BG correction
     let correctionInsulin: Decimal // Insulin for BG correction
     let iobInsulinReduction: Decimal // IOB reduction amount
     let iobInsulinReduction: Decimal // IOB reduction amount

+ 29 - 5
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

@@ -37,6 +37,9 @@ import Testing
         let basal: Decimal = 1.5
         let basal: Decimal = 1.5
         let fraction: Decimal = 0.8
         let fraction: Decimal = 0.8
         let maxBolus: Decimal = 10
         let maxBolus: Decimal = 10
+        let maxIOB: Decimal = 15.0
+        let maxCOB: Decimal = 120.0
+        let minPredBG: Decimal = 80.0
 
 
         // STEP 2: Create calculation input
         // STEP 2: Create calculation input
         let input = CalculationInput(
         let input = CalculationInput(
@@ -54,7 +57,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             basal: basal,
             fraction: fraction,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         )
 
 
         // STEP 3: Calculate insulin
         // STEP 3: Calculate insulin
@@ -144,6 +150,9 @@ import Testing
         let basal: Decimal = 1.5
         let basal: Decimal = 1.5
         let fraction: Decimal = 0.8
         let fraction: Decimal = 0.8
         let maxBolus: Decimal = 10
         let maxBolus: Decimal = 10
+        let maxIOB: Decimal = 15.0
+        let maxCOB: Decimal = 120.0
+        let minPredBG: Decimal = 80.0
 
 
         // STEP 2: Create calculation input
         // STEP 2: Create calculation input
         let input = CalculationInput(
         let input = CalculationInput(
@@ -161,7 +170,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             basal: basal,
             fraction: fraction,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         )
 
 
         // STEP 3: Calculate insulin with fatty meal enabled
         // STEP 3: Calculate insulin with fatty meal enabled
@@ -183,7 +195,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             basal: basal,
             fraction: fraction,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         )
         let standardResult = await calculator.calculateInsulin(input: standardInput)
         let standardResult = await calculator.calculateInsulin(input: standardInput)
 
 
@@ -224,6 +239,9 @@ import Testing
         let basal: Decimal = 1.5 // Will be added to insulin calculation when super bolus is enabled
         let basal: Decimal = 1.5 // Will be added to insulin calculation when super bolus is enabled
         let fraction: Decimal = 0.8
         let fraction: Decimal = 0.8
         let maxBolus: Decimal = 10
         let maxBolus: Decimal = 10
+        let maxIOB: Decimal = 15.0
+        let maxCOB: Decimal = 120.0
+        let minPredBG: Decimal = 80.0
 
 
         // STEP 2: Create calculation input with super bolus enabled
         // STEP 2: Create calculation input with super bolus enabled
         let input = CalculationInput(
         let input = CalculationInput(
@@ -241,7 +259,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             basal: basal,
             fraction: fraction,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         )
 
 
         // STEP 3: Calculate insulin with super bolus enabled
         // STEP 3: Calculate insulin with super bolus enabled
@@ -263,7 +284,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             basal: basal,
             fraction: fraction,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         )
         let standardResult = await calculator.calculateInsulin(input: standardInput)
         let standardResult = await calculator.calculateInsulin(input: standardInput)