ソースを参照

Allow backdating carbs and adapt forecasts and bolus recommendation accordingly

Marvin Polscheit 1 年間 前
コミット
103015d683

+ 12 - 3
Trio/Sources/APS/APSManager.swift

@@ -22,7 +22,11 @@ protocol APSManager {
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func determineBasal() async throws
     func determineBasalSync() async throws
-    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
+    func simulateDetermineBasal(
+        simulatedCarbsAmount: Decimal,
+        simulatedBolusAmount: Decimal,
+        simulatedCarbsDate: Date?
+    ) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
@@ -480,7 +484,11 @@ final class BaseAPSManager: APSManager, Injectable {
         _ = try await determineBasal()
     }
 
-    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
+    func simulateDetermineBasal(
+        simulatedCarbsAmount: Decimal,
+        simulatedBolusAmount: Decimal,
+        simulatedCarbsDate: Date? = nil
+    ) async -> Determination? {
         do {
             let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
@@ -488,11 +496,12 @@ final class BaseAPSManager: APSManager, Injectable {
                 clock: Date(),
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedBolusAmount: simulatedBolusAmount,
+                simulatedCarbsDate: simulatedCarbsDate,
                 simulation: true
             )
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in simulateDetermineBasal: \(error)"
             )
             return nil
         }

+ 8 - 4
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -119,7 +119,7 @@ final class OpenAPS {
         }
     }
 
-    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async throws -> String {
+    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: context,
@@ -136,13 +136,16 @@ final class OpenAPS {
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
 
             if let additionalCarbs = additionalCarbs {
+                let formattedDate = carbsDate.map { ISO8601DateFormatter().string(from: $0) } ?? ISO8601DateFormatter()
+                    .string(from: Date())
+
                 let additionalEntry = [
                     "carbs": Double(additionalCarbs),
-                    "actualDate": ISO8601DateFormatter().string(from: Date()),
+                    "actualDate": formattedDate,
                     "id": UUID().uuidString,
                     "note": NSNull(),
                     "protein": 0,
-                    "created_at": ISO8601DateFormatter().string(from: Date()),
+                    "created_at": formattedDate,
                     "isFPU": false,
                     "fat": 0,
                     "enteredBy": "Trio"
@@ -278,6 +281,7 @@ final class OpenAPS {
         clock: Date = Date(),
         simulatedCarbsAmount: Decimal? = nil,
         simulatedBolusAmount: Decimal? = nil,
+        simulatedCarbsDate: Date? = nil,
         simulation: Bool = false
     ) async throws -> Determination? {
         debug(.openAPS, "Start determineBasal")
@@ -287,7 +291,7 @@ final class OpenAPS {
 
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0)
+        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
         async let glucose = fetchAndProcessGlucose()
         async let oref2 = oref2()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)

+ 3 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -40861,6 +40861,9 @@
         }
       }
     },
+    "Backdated carbs (%lld g) included in COB calculation" : {
+
+    },
     "Backfill Failed" : {
       "localizations" : {
         "bg" : {

+ 11 - 2
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -380,12 +380,17 @@ extension Treatments {
                 minPredBG
             }
 
+            // Use the cob value of the simulation if carbs are backdated, otherwise set `simulatedCOB` to nil so that the cob value of the most recent determination gets used in the Bolus Calc Manager
+            let simulatedCOB: Int16? = (simulatedDetermination != nil && date < Date()) ?
+                Int16(truncating: NSNumber(value: (simulatedDetermination?.cob as NSDecimalNumber?)?.doubleValue ?? 0)) : nil
+
             let result = await bolusCalculationManager.handleBolusCalculation(
                 carbs: carbs,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
                 useSuperBolus: useSuperBolus,
                 lastLoopDate: apsManager.lastLoopDate,
-                minPredBG: localMinPredBG
+                minPredBG: localMinPredBG,
+                simulatedCOB: simulatedCOB
             )
 
             // Update state properties with calculation results on main thread
@@ -834,7 +839,11 @@ extension Treatments.StateModel {
         } else {
             simulatedDetermination = await Task { [self] in
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
-                return await apsManager.simulateDetermineBasal(simulatedCarbsAmount: carbs, simulatedBolusAmount: amount)
+                return await apsManager.simulateDetermineBasal(
+                    simulatedCarbsAmount: carbs,
+                    simulatedBolusAmount: amount,
+                    simulatedCarbsDate: date
+                )
             }.value
 
             // Update evBG and minPredBG from simulated determination

+ 8 - 2
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -50,10 +50,16 @@ struct ForecastChart: View {
     }
 
     private var forecastChartLabels: some View {
-        HStack {
+        // Check if carbs are backdated (date is in the past)
+        let isBackdated = state.date < Date()
+
+        // When backdated, display no carbs as this label is only supposed to show current entered carbs
+        let displayedCarbs = isBackdated ? 0 : state.carbs
+
+        return HStack {
             HStack {
                 Image(systemName: "fork.knife")
-                Text("\(state.carbs.description) g")
+                Text("\(displayedCarbs.description) g")
             }
             .font(.footnote)
             .foregroundStyle(.orange)

+ 17 - 3
Trio/Sources/Modules/Treatments/View/PopupView.swift

@@ -312,7 +312,14 @@ struct PopupView: View {
     /// Don't allow total carbs to exceed Max IOB setting.
     /// Formula: (Current COB + New Carbs) / Carb Ratio = COB Correction Dose
     private var cobCardContent: some View {
-        let hasExceededMaxCOB: Bool = Decimal(state.cob) + state.carbs > state.maxCOB
+        // Check if carbs are backdated (date is in the past)
+        let isBackdated = state.date < Date()
+
+        // Determine COB and carbs to display based on backdating status
+        let displayedCOB = isBackdated ? (state.simulatedDetermination?.cob ?? Decimal(state.cob)) : Decimal(state.cob)
+        let displayedCarbs = isBackdated ? 0 : state.carbs
+
+        let hasExceededMaxCOB: Bool = displayedCOB + displayedCarbs > state.maxCOB
         return Group {
             Grid(alignment: .center) {
                 // Row 1: Column headers for the COB calculation
@@ -333,11 +340,11 @@ struct PopupView: View {
                 GridRow {
                     Text("(")
                         .operatorStyle()
-                    Text(Int(state.cob).description)
+                    Text(Int(displayedCOB).description)
                         .valueStyle()
                     Text("+")
                         .operatorStyle()
-                    Text(Int(state.carbs).description)
+                    Text(Int(displayedCarbs).description)
                         .valueStyle()
                     Text(")")
                         .operatorStyle()
@@ -378,6 +385,13 @@ struct PopupView: View {
             }
             .multilineTextAlignment(.center)
 
+            if isBackdated {
+                Text("Backdated carbs (\(Int(state.carbs)) g) included in COB calculation")
+                    .font(.caption)
+                    .foregroundStyle(.orange)
+                    .padding(.top, 4)
+            }
+
             // Additional grid only displayed when Max COB limit has been exceeded
             if hasExceededMaxCOB {
                 Grid(alignment: .center) {

+ 8 - 0
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -224,6 +224,14 @@ extension Treatments {
                                         displayedComponents: [.hourAndMinute]
                                     ).controlSize(.mini)
                                         .labelsHidden()
+                                        .onChange(of: state.date) { _, _ in
+                                            // Trigger simulation when date changes to update forecasts for backdated carbs
+                                            Task {
+                                                // `updateForecasts()` does update the `simulatedDetermination` of type `Determination?` var on the main thread, so I can use this to pass its cob value into the bolus calc manager
+                                                await state.updateForecasts()
+                                                state.insulinCalculated = await state.calculateInsulin()
+                                            }
+                                        }
                                     Button {
                                         state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
                                     }

+ 12 - 7
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -9,9 +9,9 @@ protocol BolusCalculationManager {
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool,
         lastLoopDate: Date,
-        minPredBG: Decimal?
-    ) async
-        -> CalculationResult
+        minPredBG: Decimal?,
+        simulatedCOB: Int16?
+    ) async -> CalculationResult
 }
 
 final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
@@ -289,7 +289,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool,
         lastLoopDate: Date,
-        minPredBG: Decimal?
+        minPredBG: Decimal?,
+        simulatedCOB: Int16?
     ) async throws -> CalculationInput {
         do {
             // Get settings
@@ -345,7 +346,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 isf: bolusVars.isf,
                 carbRatio: bolusVars.carbRatio,
                 iob: bolusVars.iob,
-                cob: bolusVars.cob,
+                cob: simulatedCOB ?? bolusVars.cob,
                 useFattyMealCorrectionFactor: useFattyMealCorrection,
                 fattyMealFactor: settings.fattyMealFactor,
                 useSuperBolus: useSuperBolus,
@@ -384,6 +385,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         debug(.default, "15min insulin: \(fifteenMinutesInsulin)")
 
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
+        // we need to take backdated carbs into account - so we are using a freshly created (simulated) COB if carbs are backdated, otherwise it will default to the mostRecentDeterminations' COB value (as before)
         let wholeCob = min(Decimal(input.cob) + input.carbs, input.maxCOB)
         let wholeCobInsulin = wholeCob / input.carbRatio
         debug(.default, "Whole COB: \(wholeCob), COB insulin: \(wholeCobInsulin)")
@@ -483,13 +485,15 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     ///   - useFattyMealCorrection: Whether to apply fatty meal correction
     ///   - useSuperBolus: Whether to use super bolus calculation
     ///   - minPredBG: Minimum Predicted Glucose determined by Oref
+    ///   - simulatedCOB: Optional simulated COB from backdated entries (if available)
     /// - Returns: CalculationResult containing the calculated insulin dose and details
     func handleBolusCalculation(
         carbs: Decimal,
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool,
         lastLoopDate: Date,
-        minPredBG: Decimal? = nil
+        minPredBG: Decimal? = nil,
+        simulatedCOB: Int16? = nil
     ) async -> CalculationResult {
         do {
             let input = try await prepareCalculationInput(
@@ -497,7 +501,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 useFattyMealCorrection: useFattyMealCorrection,
                 useSuperBolus: useSuperBolus,
                 lastLoopDate: lastLoopDate,
-                minPredBG: minPredBG
+                minPredBG: minPredBG,
+                simulatedCOB: simulatedCOB
             )
             let result = await calculateInsulin(input: input)
             return result

+ 2 - 1
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -622,7 +622,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         useFattyMealCorrection: false,
                         useSuperBolus: false,
                         lastLoopDate: apsManager.lastLoopDate,
-                        minPredBG: minPredBG
+                        minPredBG: minPredBG,
+                        simulatedCOB: nil // TODO:
                     )
 
                     // Send recommendation back to watch