polscm32 1 год назад
Родитель
Сommit
40afd01c59
61 измененных файлов с 1968 добавлено и 1548 удалено
  1. 32 22
      Model/CoreDataStack.swift
  2. 30 18
      Model/Helper/CoreDataError.swift
  3. 194 198
      Trio/Sources/APS/APSManager.swift
  4. 1 1
      Trio/Sources/APS/DeviceDataManager.swift
  5. 69 60
      Trio/Sources/APS/FetchGlucoseManager.swift
  6. 1 1
      Trio/Sources/APS/FetchTreatmentsManager.swift
  7. 26 26
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  8. 17 17
      Trio/Sources/APS/Storage/CarbsStorage.swift
  9. 32 27
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  10. 6 8
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  11. 41 35
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  12. 27 33
      Trio/Sources/APS/Storage/OverrideStorage.swift
  13. 13 13
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  14. 51 50
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  15. 9 2
      Trio/Sources/Application/AppDelegate.swift
  16. 125 84
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  17. 55 35
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  18. 1 1
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  19. 1 1
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  20. 1 1
      Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  21. 11 4
      Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  22. 1 1
      Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  23. 1 1
      Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift
  24. 3 3
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  25. 13 6
      Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  26. 20 10
      Trio/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  27. 20 14
      Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  28. 19 10
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  29. 13 5
      Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  30. 24 13
      Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  31. 24 9
      Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  32. 26 12
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  33. 1 1
      Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  34. 18 13
      Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  35. 15 0
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  36. 30 26
      Trio/Sources/Modules/Stat/StatStateModel.swift
  37. 1 1
      Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift
  38. 140 108
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  39. 98 65
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  40. 13 13
      Trio/Sources/Services/Calendar/CalendarManager.swift
  41. 69 64
      Trio/Sources/Services/ContactImage/ContactImageManager.swift
  42. 103 74
      Trio/Sources/Services/HealthKit/HealthKitManager.swift
  43. 6 6
      Trio/Sources/Services/LiveActivity/Data/DataManager.swift
  44. 3 3
      Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift
  45. 64 18
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  46. 116 96
      Trio/Sources/Services/Network/TidepoolManager.swift
  47. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+APNS.swift
  48. 8 7
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift
  49. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  50. 37 23
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift
  51. 13 13
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift
  52. 4 4
      Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift
  53. 3 3
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  54. 155 136
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  55. 105 90
      Trio/Sources/Services/WatchManager/GarminManager.swift
  56. 1 1
      Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  57. 2 2
      Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift
  58. 45 48
      Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  59. 2 2
      Trio/Sources/Shortcuts/State/StateIntentRequest.swift
  60. 1 1
      Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift
  61. 4 5
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

+ 32 - 22
Model/CoreDataStack.swift

@@ -214,7 +214,9 @@ extension CoreDataStack {
         _ objectType: T.Type,
         dateKey: String,
         days: Int,
-        isPresetKey: String? = nil
+        isPresetKey: String? = nil,
+        callingFunction: String = #function,
+        callingClass: String = #fileID
     ) async throws {
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
@@ -254,14 +256,14 @@ extension CoreDataStack {
                       let success = batchDeleteResult.result as? Bool, success
                 else {
                     debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-                    throw CoreDataError.batchDeleteError
+                    throw CoreDataError.batchDeleteError(function: callingFunction, file: callingClass)
                 }
             }
 
             debugPrint("Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
         } catch {
             debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
-            throw CoreDataError.batchDeleteError
+            throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
     }
 
@@ -270,7 +272,9 @@ extension CoreDataStack {
         childType: Child.Type,
         dateKey: String,
         days: Int,
-        relationshipKey: String // The key of the Child Entity that links to the parent Entity
+        relationshipKey: String, // The key of the Child Entity that links to the parent Entity
+        callingFunction: String = #function,
+        callingClass: String = #fileID
     ) async throws {
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
@@ -316,7 +320,7 @@ extension CoreDataStack {
                       let success = batchDeleteResult.result as? Bool, success
                 else {
                     debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-                    throw CoreDataError.batchDeleteError
+                    throw CoreDataError.batchDeleteError(function: callingFunction, file: callingClass)
                 }
             }
 
@@ -325,7 +329,7 @@ extension CoreDataStack {
             )
         } catch {
             debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
-            throw CoreDataError.batchDeleteError
+            throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
     }
 }
@@ -346,7 +350,7 @@ extension CoreDataStack {
         propertiesToFetch: [String]? = nil,
         callingFunction: String = #function,
         callingClass: String = #fileID
-    ) -> [Any] {
+    ) throws -> [Any] {
         let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
@@ -367,7 +371,7 @@ extension CoreDataStack {
         context.transactionAuthor = "fetchEntities"
 
         /// we need to ensure that the fetch immediately returns a value as long as the whole app does not use the async await pattern, otherwise we could perform this asynchronously with backgroundContext.perform and not block the thread
-        return context.performAndWait {
+        return try context.performAndWait {
             do {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -375,11 +379,10 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                 }
             } catch let error as NSError {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
+                throw CoreDataError.fetchError(
+                    function: callingFunction,
+                    file: callingClass
                 )
-
-                return []
             }
         }
     }
@@ -397,10 +400,11 @@ extension CoreDataStack {
         relationshipKeyPathsForPrefetching: [String]? = nil,
         callingFunction: String = #function,
         callingClass: String = #fileID
-    ) async -> Any {
+    ) async throws -> Any {
         let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
+
         if let limit = fetchLimit {
             request.fetchLimit = limit
         }
@@ -420,7 +424,7 @@ extension CoreDataStack {
         context.name = "fetchContext"
         context.transactionAuthor = "fetchEntities"
 
-        return await context.perform {
+        return try await context.perform {
             do {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -428,10 +432,11 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                 }
             } catch let error as NSError {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
+                throw CoreDataError.unexpectedError(
+                    error: error,
+                    function: callingFunction,
+                    file: callingClass
                 )
-                return []
             }
         }
     }
@@ -439,9 +444,11 @@ extension CoreDataStack {
     // Get NSManagedObject
     func getNSManagedObject<T: NSManagedObject>(
         with ids: [NSManagedObjectID],
-        context: NSManagedObjectContext
-    ) async -> [T] {
-        await context.perform {
+        context: NSManagedObjectContext,
+        callingFunction: String = #function,
+        callingClass: String = #fileID
+    ) async throws -> [T] {
+        try await context.perform {
             var objects = [T]()
             do {
                 for id in ids {
@@ -449,10 +456,13 @@ extension CoreDataStack {
                         objects.append(object)
                     }
                 }
+                return objects
             } catch {
-                debugPrint("Failed to fetch objects: \(error.localizedDescription)")
+                throw CoreDataError.fetchError(
+                    function: callingFunction,
+                    file: callingClass
+                )
             }
-            return objects
         }
     }
 }

+ 30 - 18
Model/Helper/CoreDataError.swift

@@ -1,29 +1,41 @@
 import Foundation
 
 enum CoreDataError: Error {
-    case creationError
-    case batchInsertError
-    case batchDeleteError
-    case persistentHistoryChangeError
-    case unexpectedError(error: Error)
-    case fetchError
+    case validationError(function: String, file: String)
+    case creationError(function: String, file: String)
+    case batchInsertError(function: String, file: String)
+    case batchDeleteError(function: String, file: String)
+    case persistentHistoryChangeError(function: String, file: String)
+    case unexpectedError(error: Error, function: String, file: String)
+    case fetchError(function: String, file: String)
 }
 
 extension CoreDataError: LocalizedError {
     var errorDescription: String? {
         switch self {
-        case .creationError:
-            return NSLocalizedString("Failed to create a new object.", comment: "")
-        case .batchInsertError:
-            return NSLocalizedString("Failed to execute a batch insert request.", comment: "")
-        case .batchDeleteError:
-            return NSLocalizedString("Failed to execute a batch delete request.", comment: "")
-        case .persistentHistoryChangeError:
-            return NSLocalizedString("Failed to execute a persistent history change request.", comment: "")
-        case let .unexpectedError(error):
-            return NSLocalizedString("Received unexpected error. \(error.localizedDescription)", comment: "")
-        case .fetchError:
-            return NSLocalizedString("Failed to fetch object \(DebuggingIdentifiers.failed).", comment: "")
+        case let .creationError(function, file):
+            return NSLocalizedString("Failed to create a new object in \(function) from \(file).", comment: "")
+        case let .batchInsertError(function, file):
+            return NSLocalizedString("Failed to execute a batch insert request in \(function) from \(file).", comment: "")
+        case let .batchDeleteError(function, file):
+            return NSLocalizedString("Failed to execute a batch delete request in \(function) from \(file).", comment: "")
+        case let .persistentHistoryChangeError(function, file):
+            return NSLocalizedString(
+                "Failed to execute a persistent history change request in \(function) from \(file).",
+                comment: ""
+            )
+        case let .unexpectedError(error, function, file):
+            return NSLocalizedString(
+                "Received unexpected error in \(function) from \(file): \(error.localizedDescription)",
+                comment: ""
+            )
+        case let .fetchError(function, file):
+            return NSLocalizedString(
+                "Failed to fetch object \(DebuggingIdentifiers.failed) in \(function) from \(file).",
+                comment: ""
+            )
+        case let .validationError(function, file):
+            return NSLocalizedString("Failed to validate object in \(function) from \(file).", comment: "")
         }
     }
 }

+ 194 - 198
Trio/Sources/APS/APSManager.swift

@@ -20,7 +20,7 @@ protocol APSManager {
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     func enactTempBasal(rate: Double, duration: TimeInterval) async
-    func determineBasal() async -> Bool
+    func determineBasal() async throws -> Bool
     func determineBasalSync() async
     func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
@@ -130,7 +130,14 @@ final class BaseAPSManager: APSManager, Injectable {
             let wasParsed = storage.parseOnFileSettingsToMgdL()
             if wasParsed {
                 Task {
-                    await openAPS.createProfiles()
+                    do {
+                        try await openAPS.createProfiles()
+                    } catch {
+                        debug(
+                            .apsManager,
+                            "\(DebuggingIdentifiers.failed) Error creating profiles: \(error.localizedDescription)"
+                        )
+                    }
                 }
             }
         }
@@ -242,7 +249,7 @@ final class BaseAPSManager: APSManager, Injectable {
             isLooping.send(true)
 
             do {
-                if await !determineBasal() {
+                if try await !determineBasal() {
                     throw APSError.apsError(message: "Determine basal failed")
                 }
 
@@ -346,11 +353,11 @@ final class BaseAPSManager: APSManager, Injectable {
         return false
     }
 
-    func determineBasal() async -> Bool {
+    func determineBasal() async throws -> Bool {
         debug(.apsManager, "Start determine basal")
 
         // Fetch glucose asynchronously
-        let glucose = await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
+        let glucose = try await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
 
         // Perform the context-related checks and actions
         let isValidGlucoseData = await privateContext.perform {
@@ -390,7 +397,7 @@ final class BaseAPSManager: APSManager, Injectable {
             async let autosenseResult = autosense()
 
             _ = try await autosenseResult
-            await openAPS.createProfiles()
+            try await openAPS.createProfiles()
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
 
             if let determination = determination {
@@ -410,12 +417,19 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     func determineBasalSync() async {
-        _ = await determineBasal()
+        do {
+            _ = try await determineBasal()
+        } catch {
+            debug(
+                .apsManager,
+                "\(DebuggingIdentifiers.failed) Error performing determine basal sync: \(error.localizedDescription)"
+            )
+        }
     }
 
     func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination? {
         do {
-            let temp = await fetchCurrentTempBasal(date: Date.now)
+            let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(currentTemp: temp, clock: Date(), carbs: carbs, iob: iob, simulation: true)
         } catch {
             debugPrint(
@@ -522,8 +536,8 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func fetchCurrentTempBasal(date: Date) async -> TempBasal {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchCurrentTempBasal(date: Date) async throws -> TempBasal {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: privateContext,
             predicate: NSPredicate.recentPumpHistory,
@@ -562,7 +576,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func enactDetermination() async throws {
-        guard let determinationID = await determinationStorage
+        guard let determinationID = try await determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
         else {
             throw APSError.apsError(message: "Determination not found")
@@ -617,35 +631,39 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func reportEnacted(wasEnacted: Bool) async {
-        guard let determinationID = await determinationStorage
-            .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
-        else {
-            return
-        }
-        await privateContext.perform {
-            if let determinationUpdated = self.privateContext.object(with: determinationID) as? OrefDetermination {
+        do {
+            guard let determinationID = try await determinationStorage
+                .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
+            else {
+                debug(.apsManager, "No determination found to report enacted status")
+                return
+            }
+
+            try await privateContext.perform {
+                guard let determinationUpdated = try self.privateContext
+                    .existingObject(with: determinationID) as? OrefDetermination
+                else {
+                    debug(.apsManager, "Could not find determination object in context")
+                    return
+                }
+
                 determinationUpdated.timestamp = Date()
                 determinationUpdated.enacted = wasEnacted
                 determinationUpdated.isUploadedToNS = false
 
-                do {
-                    guard self.privateContext.hasChanges else { return }
-                    try self.privateContext.save()
-                    debugPrint("Update successful in reportEnacted() \(DebuggingIdentifiers.succeeded)")
-                } catch {
-                    debugPrint(
-                        "Failed  \(DebuggingIdentifiers.succeeded) to save context in reportEnacted(): \(error.localizedDescription)"
-                    )
-                }
-
+                guard self.privateContext.hasChanges else { return }
+                try self.privateContext.save()
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
 
                 Task.detached(priority: .low) {
                     await self.statistics()
                 }
-            } else {
-                debugPrint("Failed to update OrefDetermination in reportEnacted()")
             }
+        } catch {
+            debug(
+                .apsManager,
+                "\(DebuggingIdentifiers.failed) Error reporting enacted status: \(error.localizedDescription)"
+            )
         }
     }
 
@@ -805,8 +823,8 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     // fetch glucose for time interval
-    func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async -> [GlucoseStored] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async throws -> [GlucoseStored] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: privateContext,
             predicate: predicate,
@@ -843,7 +861,7 @@ final class BaseAPSManager: APSManager, Injectable {
             async let carbTotal = carbsForStats()
             async let preferences = settingsManager.preferences
 
-            let loopStats = await loopStats(oneDayGlucose: await glucoseStats.oneDayGlucose.readings)
+            let loopStats = await loopStats(oneDayGlucose: Double(rawValue: (await glucoseStats?.oneDayGlucose.readings)!) ?? 0.0)
 
             // Only save and upload once per day
             guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
@@ -913,7 +931,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 scheduled_basal: 0,
                 total_average: 0
             )
-            let processedGlucoseStats = await glucoseStats
+            guard let processedGlucoseStats = await glucoseStats else { return }
             let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
 
             let dailystat = await Statistics(
@@ -932,7 +950,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 insulinType: insulin_type.rawValue,
                 peakActivityTime: iPa,
                 Carbs_24h: await carbTotal,
-                GlucoseStorage_Days: Decimal(roundDouble(processedGlucoseStats.numberofDays, 1)),
+                GlucoseStorage_Days: Decimal(roundDouble(Double(rawValue: processedGlucoseStats.numberofDays) ?? 0.0, 1)),
                 Statistics: Stats(
                     Distribution: processedGlucoseStats.TimeInRange,
                     Glucose: processedGlucoseStats.avg,
@@ -1092,175 +1110,153 @@ final class BaseAPSManager: APSManager, Injectable {
         return (currentTDD, tddTotalAverage)
     }
 
-    private func glucoseForStats() async
-        -> (
-            oneDayGlucose: (
-                ifcc: Double,
-                ngsp: Double,
-                average: Double,
-                median: Double,
-                sd: Double,
-                cv: Double,
-                readings: Double
-            ),
-            hbA1cDisplayUnit: HbA1cDisplayUnit,
-            numberofDays: Double,
-            TimeInRange: TIRs,
-            avg: Averages,
-            hbs: Durations,
-            variance: Variance
-        )
-    {
-        // Get the Glucose Values
-        let glucose24h = await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
-        let glucoseOneWeek = await fetchGlucose(
-            predicate: NSPredicate.predicateForOneWeek,
-            fetchLimit: 288 * 7,
-            batchSize: 250
-        )
-        let glucoseOneMonth = await fetchGlucose(
-            predicate: NSPredicate.predicateForOneMonth,
-            fetchLimit: 288 * 7 * 30,
-            batchSize: 500
-        )
-        let glucoseThreeMonths = await fetchGlucose(
-            predicate: NSPredicate.predicateForThreeMonths,
-            fetchLimit: 288 * 7 * 30 * 3,
-            batchSize: 1000
-        )
+    private func glucoseForStats() async -> (
+        oneDayGlucose: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double),
+        hbA1cDisplayUnit: HbA1cDisplayUnit,
+        numberofDays: Double,
+        TimeInRange: TIRs,
+        avg: Averages,
+        hbs: Durations,
+        variance: Variance
+    )? {
+        do {
+            // Get the Glucose Values
+            let glucose24h = try await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
+            let glucoseOneWeek = try await fetchGlucose(
+                predicate: NSPredicate.predicateForOneWeek,
+                fetchLimit: 288 * 7,
+                batchSize: 250
+            )
+            let glucoseOneMonth = try await fetchGlucose(
+                predicate: NSPredicate.predicateForOneMonth,
+                fetchLimit: 288 * 7 * 30,
+                batchSize: 500
+            )
+            let glucoseThreeMonths = try await fetchGlucose(
+                predicate: NSPredicate.predicateForThreeMonths,
+                fetchLimit: 288 * 7 * 30 * 3,
+                batchSize: 1000
+            )
 
-        var result: (
-            oneDayGlucose: (
-                ifcc: Double,
-                ngsp: Double,
-                average: Double,
-                median: Double,
-                sd: Double,
-                cv: Double,
-                readings: Double
-            ),
-            hbA1cDisplayUnit: HbA1cDisplayUnit,
-            numberofDays: Double,
-            TimeInRange: TIRs,
-            avg: Averages,
-            hbs: Durations,
-            variance: Variance
-        )?
+            return await privateContext.perform {
+                let units = self.settingsManager.settings.units
+
+                // First date
+                let previous = glucoseThreeMonths.last?.date ?? Date()
+                // Last date (recent)
+                let current = glucoseThreeMonths.first?.date ?? Date()
+                // Total time in days
+                let numberOfDays = (current - previous).timeInterval / 8.64E4
+
+                // Get glucose computations for every case
+                let oneDayGlucose = self.glucoseStats(glucose24h)
+                let sevenDaysGlucose = self.glucoseStats(glucoseOneWeek)
+                let thirtyDaysGlucose = self.glucoseStats(glucoseOneMonth)
+                let totalDaysGlucose = self.glucoseStats(glucoseThreeMonths)
+
+                let median = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.median), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.median), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
+                )
 
-        await privateContext.perform {
-            let units = self.settingsManager.settings.units
-
-            // First date
-            let previous = glucoseThreeMonths.last?.date ?? Date()
-            // Last date (recent)
-            let current = glucoseThreeMonths.first?.date ?? Date()
-            // Total time in days
-            let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-            // Get glucose computations for every case
-            let oneDayGlucose = self.glucoseStats(glucose24h)
-            let sevenDaysGlucose = self.glucoseStats(glucoseOneWeek)
-            let thirtyDaysGlucose = self.glucoseStats(glucoseOneMonth)
-            let totalDaysGlucose = self.glucoseStats(glucoseThreeMonths)
-
-            let median = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.median), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.median), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
-            )
+                let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
+
+                let hbs = Durations(
+                    day: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
+                    week: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
+                    month: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
+                    total: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
+                )
 
-            let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
-
-            let hbs = Durations(
-                day: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                week: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                month: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                total: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
-            )
+                var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                // Get TIR computations for every case
+                oneDay_ = self.tir(glucose24h)
+                sevenDays_ = self.tir(glucoseOneWeek)
+                thirtyDays_ = self.tir(glucoseOneMonth)
+                totalDays_ = self.tir(glucoseThreeMonths)
+
+                let tir = Durations(
+                    day: self.roundDecimal(Decimal(oneDay_.TIR), 1),
+                    week: self.roundDecimal(Decimal(sevenDays_.TIR), 1),
+                    month: self.roundDecimal(Decimal(thirtyDays_.TIR), 1),
+                    total: self.roundDecimal(Decimal(totalDays_.TIR), 1)
+                )
+                let hypo = Durations(
+                    day: Decimal(oneDay_.hypos),
+                    week: Decimal(sevenDays_.hypos),
+                    month: Decimal(thirtyDays_.hypos),
+                    total: Decimal(totalDays_.hypos)
+                )
+                let hyper = Durations(
+                    day: Decimal(oneDay_.hypers),
+                    week: Decimal(sevenDays_.hypers),
+                    month: Decimal(thirtyDays_.hypers),
+                    total: Decimal(totalDays_.hypers)
+                )
+                let normal = Durations(
+                    day: Decimal(oneDay_.normal_),
+                    week: Decimal(sevenDays_.normal_),
+                    month: Decimal(thirtyDays_.normal_),
+                    total: Decimal(totalDays_.normal_)
+                )
+                let range = Threshold(
+                    low: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.low.asMmolL, 1) :
+                        self.roundDecimal(self.settingsManager.settings.low, 0),
+                    high: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.high.asMmolL, 1) :
+                        self.roundDecimal(self.settingsManager.settings.high, 0)
+                )
+                let TimeInRange = TIRs(
+                    TIR: tir,
+                    Hypos: hypo,
+                    Hypers: hyper,
+                    Threshold: range,
+                    Euglycemic: normal
+                )
+                let avgs = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.average), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.average), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.average), 1)
+                )
+                let avg = Averages(Average: avgs, Median: median)
+                // Standard Deviations
+                let standardDeviations = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.sd), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.sd), 1)
+                )
+                // CV = standard deviation / sample mean x 100
+                let cvs = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.cv), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.cv), 1)
+                )
+                let variance = Variance(SD: standardDeviations, CV: cvs)
 
-            var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            // Get TIR computations for every case
-            oneDay_ = self.tir(glucose24h)
-            sevenDays_ = self.tir(glucoseOneWeek)
-            thirtyDays_ = self.tir(glucoseOneMonth)
-            totalDays_ = self.tir(glucoseThreeMonths)
-
-            let tir = Durations(
-                day: self.roundDecimal(Decimal(oneDay_.TIR), 1),
-                week: self.roundDecimal(Decimal(sevenDays_.TIR), 1),
-                month: self.roundDecimal(Decimal(thirtyDays_.TIR), 1),
-                total: self.roundDecimal(Decimal(totalDays_.TIR), 1)
-            )
-            let hypo = Durations(
-                day: Decimal(oneDay_.hypos),
-                week: Decimal(sevenDays_.hypos),
-                month: Decimal(thirtyDays_.hypos),
-                total: Decimal(totalDays_.hypos)
-            )
-            let hyper = Durations(
-                day: Decimal(oneDay_.hypers),
-                week: Decimal(sevenDays_.hypers),
-                month: Decimal(thirtyDays_.hypers),
-                total: Decimal(totalDays_.hypers)
-            )
-            let normal = Durations(
-                day: Decimal(oneDay_.normal_),
-                week: Decimal(sevenDays_.normal_),
-                month: Decimal(thirtyDays_.normal_),
-                total: Decimal(totalDays_.normal_)
-            )
-            let range = Threshold(
-                low: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.low.asMmolL, 1) :
-                    self.roundDecimal(self.settingsManager.settings.low, 0),
-                high: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.high.asMmolL, 1) :
-                    self.roundDecimal(self.settingsManager.settings.high, 0)
-            )
-            let TimeInRange = TIRs(
-                TIR: tir,
-                Hypos: hypo,
-                Hypers: hyper,
-                Threshold: range,
-                Euglycemic: normal
-            )
-            let avgs = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.average), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.average), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.average), 1)
-            )
-            let avg = Averages(Average: avgs, Median: median)
-            // Standard Deviations
-            let standardDeviations = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.sd), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.sd), 1)
-            )
-            // CV = standard deviation / sample mean x 100
-            let cvs = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.cv), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.cv), 1)
+                return (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+            }
+        } catch {
+            debug(
+                .apsManager,
+                "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)"
             )
-            let variance = Variance(SD: standardDeviations, CV: cvs)
-
-            result = (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+            return nil
         }
-
-        return result!
     }
 
     private func loopStats(loopStatRecord: LoopStats) {

+ 1 - 1
Trio/Sources/APS/DeviceDataManager.swift

@@ -523,7 +523,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
                 guard let type = $0.type, type == .tempBasal else { return true }
                 return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal)
             }
-            await pumpHistoryStorage.storePumpEvents(events)
+            try await pumpHistoryStorage.storePumpEvents(events)
             lastEventDate = events.last?.date
             completion(nil)
         }

+ 69 - 60
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -8,6 +8,7 @@ import Swinject
 import UIKit
 
 protocol FetchGlucoseManager: SourceInfoProvider {
+    func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource()
     func removeCalibrations()
@@ -75,6 +76,44 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         subscribe()
     }
 
+    /// The function used to start the timer sync - Function of the variable defined in config
+    private func subscribe() {
+        timer.publisher
+            .receive(on: processQueue)
+            .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
+                debug(.nightscout, "FetchGlucoseManager timer heartbeat")
+                if let glucoseSource = self.glucoseSource {
+                    return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
+                } else {
+                    return Empty(completeImmediately: false).eraseToAnyPublisher()
+                }
+            }
+            .sink { glucose in
+                debug(.nightscout, "FetchGlucoseManager callback sensor")
+                Publishers.CombineLatest(
+                    Just(glucose),
+                    Just(self.glucoseStorage.syncDate())
+                )
+                .eraseToAnyPublisher()
+                .sink { newGlucose, syncDate in
+                    Task {
+                        do {
+                            try await self.glucoseStoreAndHeartDecision(
+                                syncDate: syncDate,
+                                glucose: newGlucose
+                            )
+                        } catch {
+                            debug(.deviceManager, "Failed to store glucose: \(error.localizedDescription)")
+                        }
+                    }
+                }
+                .store(in: &self.lifetime)
+            }
+            .store(in: &lifetime)
+        timer.fire()
+        timer.resume()
+    }
+
     var glucoseSource: GlucoseSource!
 
     func removeCalibrations() {
@@ -164,8 +203,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return Manager.init(rawState: rawState)
     }
 
-    private func fetchGlucose() async -> [GlucoseStored]? {
-        await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [GlucoseStored]? {
+        try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -175,10 +214,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         ) as? [GlucoseStored]
     }
 
-    private func processGlucose() async -> [BloodGlucose] {
-        guard let results = await fetchGlucose() else { return [] }
+    private func processGlucose() async throws -> [BloodGlucose] {
+        let results = try await fetchGlucose()
+        guard let results else { return [] }
+
         return await context.perform {
-            return results.map { result in
+            results.map { result in
                 BloodGlucose(
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
@@ -194,7 +235,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
     }
 
-    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async {
+    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
 
@@ -209,31 +250,27 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             backGroundFetchBGTaskID = .invalid
         }
 
-        guard newGlucose.isNotEmpty else {
+        defer {
             if let backgroundTask = backGroundFetchBGTaskID {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
+                Task {
+                    await UIApplication.shared.endBackgroundTask(backgroundTask)
+                }
                 backGroundFetchBGTaskID = .invalid
             }
-            return
         }
 
+        guard newGlucose.isNotEmpty else { return }
+
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
-        guard filtered.isNotEmpty else {
-            // end of the Background tasks
-            if let backgroundTask = backGroundFetchBGTaskID {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundFetchBGTaskID = .invalid
-            }
-            return
-        }
+        guard filtered.isNotEmpty else { return }
         debug(.deviceManager, "New glucose found")
 
         // filter the data if it is the case
         if settingsManager.settings.smoothGlucose {
             // limited to 30 min of old glucose data
-            let oldGlucoseValues = await processGlucose()
+            let oldGlucoseValues = try await processGlucose()
 
             var smoothedValues = oldGlucoseValues + filtered
             // smooth with 3 repeats
@@ -244,49 +281,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             filtered = smoothedValues.filter { $0.dateString > syncDate }
         }
 
-        await glucoseStorage.storeGlucose(filtered)
-
+        try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
-
-        // End of the Background tasks
-        if let backgroundTask = backGroundFetchBGTaskID {
-            await UIApplication.shared.endBackgroundTask(backgroundTask)
-            backGroundFetchBGTaskID = .invalid
-        }
-    }
-
-    /// The function used to start the timer sync - Function of the variable defined in config
-    private func subscribe() {
-        timer.publisher
-            .receive(on: processQueue)
-            .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
-                debug(.nightscout, "FetchGlucoseManager timer heartbeat")
-                if let glucoseSource = self.glucoseSource {
-                    return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
-                } else {
-                    return Empty(completeImmediately: false).eraseToAnyPublisher()
-                }
-            }
-            .sink { glucose in
-                debug(.nightscout, "FetchGlucoseManager callback sensor")
-                Publishers.CombineLatest(
-                    Just(glucose),
-                    Just(self.glucoseStorage.syncDate())
-                )
-                .eraseToAnyPublisher()
-                .sink { newGlucose, syncDate in
-                    Task {
-                        await self.glucoseStoreAndHeartDecision(
-                            syncDate: syncDate,
-                            glucose: newGlucose
-                        )
-                    }
-                }
-                .store(in: &self.lifetime)
-            }
-            .store(in: &lifetime)
-        timer.fire()
-        timer.resume()
     }
 
     func sourceInfo() -> [String: Any]? {
@@ -312,6 +308,19 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             return entries
         }
     }
+
+    /// function called when a callback is fired by CGM BLE
+    public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
+        Task {
+            let syncDate = glucoseStorage.syncDate()
+            debug(.deviceManager, "CGM BLE FETCHGLUCOSE  : SyncDate is \(syncDate)")
+            do {
+                try await glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
+            } catch {
+                debug(.deviceManager, "Failed to store glucose: \(error.localizedDescription)")
+            }
+        }
+    }
 }
 
 extension CGMManager {

+ 1 - 1
Trio/Sources/APS/FetchTreatmentsManager.swift

@@ -37,7 +37,7 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
                     // Filter and store if not from "Trio"
                     let filteredCarbs = await carbs.filter { $0.enteredBy != CarbsEntry.local }
                     if filteredCarbs.isNotEmpty {
-                        await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
+                        try await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
                     }
 
                     // Filter and store if not from Trio

+ 26 - 26
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -100,8 +100,8 @@ final class OpenAPS {
     }
 
     // fetch glucose to pass it to the meal function and to determine basal
-    private func fetchAndProcessGlucose() async -> String {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchAndProcessGlucose() async throws -> String {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
@@ -121,8 +121,8 @@ final class OpenAPS {
         }
     }
 
-    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async -> String {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async throws -> String {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -170,8 +170,8 @@ final class OpenAPS {
         return json
     }
 
-    private func fetchPumpHistoryObjectIDs() async -> [NSManagedObjectID]? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchPumpHistoryObjectIDs() async throws -> [NSManagedObjectID]? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
             predicate: NSPredicate.pumpHistoryLast1440Minutes,
@@ -287,10 +287,10 @@ final class OpenAPS {
             reservoir,
             preferences
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs, iob: iob),
-            carbs,
-            glucose,
-            oref2,
+            try parsePumpHistory(await pumpHistoryObjectIDs, iob: iob),
+            try carbs,
+            try glucose,
+            try oref2,
             profileAsync,
             basalAsync,
             autosenseAsync,
@@ -355,8 +355,8 @@ final class OpenAPS {
         }
     }
 
-    func oref2() async -> RawJSON {
-        await context.perform {
+    func oref2() async throws -> RawJSON {
+        try await context.perform {
             // Retrieve user preferences
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             let weightPercentage = userPreferences?.weightPercentage ?? 1.0
@@ -366,10 +366,10 @@ final class OpenAPS {
             // Fetch historical events for Total Daily Dose (TDD) calculation
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
-            let historicalTDDData = self.fetchHistoricalTDDData(from: tenDaysAgo)
+            let historicalTDDData = try self.fetchHistoricalTDDData(from: tenDaysAgo)
 
             // Fetch the last active Override
-            let activeOverrides = self.fetchActiveOverrides()
+            let activeOverrides = try self.fetchActiveOverrides()
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
@@ -433,9 +433,9 @@ final class OpenAPS {
 
         // Await the results of asynchronous tasks
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs),
-            carbs,
-            glucose,
+            try parsePumpHistory(await pumpHistoryObjectIDs),
+            try carbs,
+            try glucose,
             getProfile,
             getBasalProfile,
             getTempTargets
@@ -462,7 +462,7 @@ final class OpenAPS {
         }
     }
 
-    func createProfiles() async {
+    func createProfiles() async throws {
         debug(.openAPS, "Start creating pump profile and user profile")
 
         // Load required settings and profiles asynchronously
@@ -492,9 +492,9 @@ final class OpenAPS {
         var adjustedPreferences = preferences
 
         // Check for active Temp Targets and adjust HBT if necessary
-        await context.perform {
+        try await context.perform {
             // Check if a Temp Target is active and if its HBT differs from user preferences
-            if let activeTempTarget = self.fetchActiveTempTargets().first,
+            if let activeTempTarget = try self.fetchActiveTempTargets().first,
                activeTempTarget.enabled,
                let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
                activeHBT != defaultHalfBasalTarget
@@ -786,8 +786,8 @@ final class OpenAPS {
 
 // Non-Async fetch methods for oref2
 extension OpenAPS {
-    func fetchActiveTempTargets() -> [TempTargetStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveTempTargets() throws -> [TempTargetStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: TempTargetStored.self,
             onContext: context,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -797,8 +797,8 @@ extension OpenAPS {
         ) as? [TempTargetStored] ?? []
     }
 
-    func fetchActiveOverrides() -> [OverrideStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveOverrides() throws -> [OverrideStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: OverrideStored.self,
             onContext: context,
             predicate: NSPredicate.lastActiveOverride,
@@ -808,8 +808,8 @@ extension OpenAPS {
         ) as? [OverrideStored] ?? []
     }
 
-    func fetchHistoricalTDDData(from date: Date) -> [[String: Any]] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             onContext: context,
             predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),

+ 17 - 17
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -10,14 +10,14 @@ protocol CarbsObserver {
 
 protocol CarbsStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
+    func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async throws
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
-    func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
-    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
+    func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry]
+    func getCarbsNotYetUploadedToTidepool() async throws -> [CarbsEntry]
 }
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -38,11 +38,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
     }
 
-    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
+    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async throws {
         var entriesToStore = entries
 
         if areFetchedFromRemote {
-            entriesToStore = await filterRemoteEntries(entries: entriesToStore)
+            entriesToStore = try await filterRemoteEntries(entries: entriesToStore)
         }
 
         // Check for FPU-only entries (fat/protein without carbs)
@@ -71,9 +71,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
 
-    private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
+    private func filterRemoteEntries(entries: [CarbsEntry]) async throws -> [CarbsEntry] {
         // Fetch only the date property from Core Data
-        guard let existing24hCarbEntries = await CoreDataStack.shared.fetchEntitiesAsync(
+        guard let existing24hCarbEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -339,8 +339,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
@@ -378,8 +378,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
@@ -415,8 +415,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
@@ -446,8 +446,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getCarbsNotYetUploadedToTidepool() async throws -> [CarbsEntry] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,

+ 32 - 27
Trio/Sources/APS/Storage/ContactImageStorage.swift

@@ -26,36 +26,41 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
     ///
     /// - Returns: An array of `ContactImageEntry` objects.
     func fetchContactImageEntries() async -> [ContactImageEntry] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: ContactImageEntryStored.self,
-            onContext: backgroundContext,
-            predicate: NSPredicate.all,
-            key: "hasHighContrast",
-            ascending: false
-        )
+        do {
+            let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: ContactImageEntryStored.self,
+                onContext: backgroundContext,
+                predicate: NSPredicate.all,
+                key: "hasHighContrast",
+                ascending: false
+            )
 
-        return await backgroundContext.perform {
-            guard let fetchedContactImageEntries = results as? [ContactImageEntryStored] else { return [] }
+            return await backgroundContext.perform {
+                guard let fetchedContactImageEntries = results as? [ContactImageEntryStored] else { return [] }
 
-            return fetchedContactImageEntries.compactMap { entry in
-                ContactImageEntry(
-                    name: entry.name ?? "No name provided",
-                    layout: ContactImageLayout(rawValue: entry.layout ?? "Default") ?? .default,
-                    ring: ContactImageLargeRing(rawValue: entry.ring ?? "Hidden") ?? .none,
-                    primary: ContactImageValue(rawValue: entry.primary ?? "Glucose Reading") ?? .glucose,
-                    top: ContactImageValue(rawValue: entry.top ?? "None") ?? .none,
-                    bottom: ContactImageValue(rawValue: entry.bottom ?? "None") ?? .none,
-                    contactId: entry.contactId?.string,
-                    hasHighContrast: entry.hasHighContrast,
-                    ringWidth: ContactImageEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,
-                    ringGap: ContactImageEntry.RingGap(rawValue: Int(entry.ringGap)) ?? .small,
-                    fontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSize)) ?? .regular,
-                    secondaryFontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSizeSecondary)) ?? .small,
-                    fontWeight: Font.Weight.fromString(entry.fontWeight ?? "regular"),
-                    fontWidth: Font.Width.fromString(entry.fontWidth ?? "standard"),
-                    managedObjectID: entry.objectID
-                )
+                return fetchedContactImageEntries.compactMap { entry in
+                    ContactImageEntry(
+                        name: entry.name ?? "No name provided",
+                        layout: ContactImageLayout(rawValue: entry.layout ?? "Default") ?? .default,
+                        ring: ContactImageLargeRing(rawValue: entry.ring ?? "Hidden") ?? .none,
+                        primary: ContactImageValue(rawValue: entry.primary ?? "Glucose Reading") ?? .glucose,
+                        top: ContactImageValue(rawValue: entry.top ?? "None") ?? .none,
+                        bottom: ContactImageValue(rawValue: entry.bottom ?? "None") ?? .none,
+                        contactId: entry.contactId?.string,
+                        hasHighContrast: entry.hasHighContrast,
+                        ringWidth: ContactImageEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,
+                        ringGap: ContactImageEntry.RingGap(rawValue: Int(entry.ringGap)) ?? .small,
+                        fontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSize)) ?? .regular,
+                        secondaryFontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSizeSecondary)) ?? .small,
+                        fontWeight: Font.Weight.fromString(entry.fontWeight ?? "regular"),
+                        fontWidth: Font.Width.fromString(entry.fontWidth ?? "standard"),
+                        managedObjectID: entry.objectID
+                    )
+                }
             }
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Error fetching contact image entries: \(error.localizedDescription)")
+            return []
         }
     }
 

+ 6 - 8
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -4,7 +4,7 @@ import Foundation
 import Swinject
 
 protocol DeterminationStorage {
-    func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
+    func fetchLastDeterminationObjectID(predicate: NSPredicate) async throws -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func fetchForecastObjects(
@@ -13,7 +13,7 @@ protocol DeterminationStorage {
     ) async -> (UUID, Forecast?, [ForecastValue])
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
     func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
-    async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
+    async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
 }
 
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
@@ -25,8 +25,8 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         injectServices(resolver)
     }
 
-    func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastDeterminationObjectID(predicate: NSPredicate) async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
             predicate: predicate,
@@ -37,7 +37,6 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 
         return await context.perform {
             guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
             return fetchedResults.map(\.objectID)
         }
     }
@@ -206,10 +205,9 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     }
 
     func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
-    async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
+    async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
     {
-        // Fetch forecasts with prefetched values for the given determination
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: Forecast.self,
             onContext: context,
             predicate: NSPredicate(format: "orefDetermination = %@", determinationID),

+ 41 - 35
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -9,20 +9,20 @@ import Swinject
 
 protocol GlucoseStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storeGlucose(_ glucose: [BloodGlucose]) async
+    func storeGlucose(_ glucose: [BloodGlucose]) async throws
     func addManualGlucose(glucose: Int)
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
     func isGlucoseFresh() -> Bool
-    func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
-    func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
-    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
-    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
-    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
+    func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
+    func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
+    func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
+    func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
+    func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
     var alarm: GlucoseAlarm? { get }
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
@@ -60,8 +60,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return formatter
     }
 
-    func storeGlucose(_ glucose: [BloodGlucose]) async {
-        await coredataContext.perform {
+    func storeGlucose(_ glucose: [BloodGlucose]) async throws {
+        try await coredataContext.perform {
             // Get new glucose values that don't exist yet
             let newGlucose = self.filterNewGlucoseValues(glucose)
             guard !newGlucose.isEmpty else { return }
@@ -70,8 +70,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 // Store glucose values in Core Data
                 try self.storeGlucoseInCoreData(newGlucose)
             } catch {
-                debugPrint(
-                    "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to store glucose: \(error)"
+                throw CoreDataError.creationError(
+                    function: #function,
+                    file: #fileID
                 )
             }
 
@@ -312,9 +313,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return filtered
     }
 
-    func fetchLatestGlucose() -> GlucoseStored? {
+    func fetchLatestGlucose() throws -> GlucoseStored? {
         let predicate = NSPredicate.predicateFor20MinAgo
-        return (CoreDataStack.shared.fetchEntities(
+        return (try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: predicate,
@@ -326,8 +327,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
-    func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
@@ -357,8 +358,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
-    func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
@@ -411,8 +412,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
-    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
@@ -441,8 +442,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
-    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
@@ -471,8 +472,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch glucose that is not uploaded to Tidepool yet
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
-    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
@@ -502,8 +503,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch manual glucose that is not uploaded to Tidepool yet
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
-    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
@@ -560,19 +561,24 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         coredataContext.performAndWait {
-            guard let glucose = fetchLatestGlucose() else { return nil }
+            do {
+                guard let glucose = try fetchLatestGlucose() else { return nil }
 
-            let glucoseValue = glucose.glucose
+                let glucoseValue = glucose.glucose
 
-            if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
-                return .low
-            }
+                if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
+                    return .low
+                }
 
-            if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
-                return .high
-            }
+                if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
+                    return .high
+                }
 
-            return nil
+                return nil
+            } catch {
+                debugPrint("Error fetching latest glucose: \(error)")
+                return nil
+            }
         }
     }
 }

+ 27 - 33
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -3,17 +3,17 @@ import Foundation
 import Swinject
 
 protocol OverrideStorage {
-    func fetchLastCreatedOverride() async -> [NSManagedObjectID]
-    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
-    func fetchForOverridePresets() async -> [NSManagedObjectID]
+    func fetchLastCreatedOverride() async throws -> [NSManagedObjectID]
+    func loadLatestOverrideConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
+    func fetchForOverridePresets() async throws -> [NSManagedObjectID]
     func calculateTarget(override: OverrideStored) -> Decimal
-    func storeOverride(override: Override) async
+    func storeOverride(override: Override) async throws
     func copyRunningOverride(_ override: OverrideStored) async -> NSManagedObjectID
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
-    func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
-    func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
-    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride]
-    func fetchLatestActiveOverride() async -> NSManagedObjectID?
+    func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
+    func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
+    func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride]
+    func fetchLatestActiveOverride() async throws -> NSManagedObjectID?
 }
 
 final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
@@ -34,8 +34,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return dateFormatter
     }
 
-    func fetchLastCreatedOverride() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastCreatedOverride() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate(
@@ -54,8 +54,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
     }
 
-    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func loadLatestOverrideConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
@@ -72,8 +72,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     }
 
     /// Returns the NSManagedObjectID of the Override Presets
-    func fetchForOverridePresets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchForOverridePresets() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
@@ -95,14 +95,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return overrideTarget.decimalValue
     }
 
-    func storeOverride(override: Override) async {
+    func storeOverride(override: Override) async throws {
         var presetCount = -1
         if override.isPreset {
-            let presets = await fetchForOverridePresets()
+            let presets = try await fetchForOverridePresets()
             presetCount = presets.count
         }
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newOverride = OverrideStored(context: self.backgroundContext)
 
             // override key meta data
@@ -151,14 +151,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                 newOverride.smbIsScheduledOff = false
             }
 
-            do {
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Preset to Core Data with error: \(error.userInfo)"
-                )
-            }
+            guard self.backgroundContext.hasChanges else { return }
+            try self.backgroundContext.save()
         }
     }
 
@@ -206,8 +200,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
     }
 
-    func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout,
@@ -232,8 +226,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
     }
 
-    func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate(
@@ -263,8 +257,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
     }
 
-    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
@@ -290,8 +284,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
     }
 
-    func fetchLatestActiveOverride() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLatestActiveOverride() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,

+ 13 - 13
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -11,11 +11,11 @@ protocol PumpHistoryObserver {
 
 protocol PumpHistoryStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storePumpEvents(_ events: [NewPumpEvent]) async
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
-    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
-    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getPumpHistoryNotYetUploadedToHealth() async throws -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async throws -> [PumpHistoryEvent]
 }
 
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
@@ -44,10 +44,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         return Decimal(roundedValue)
     }
 
-    func storePumpEvents(_ events: [NewPumpEvent]) async {
-        await context.perform {
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws {
+        try await context.perform {
             for event in events {
-                let existingEvents: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
+                let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
                     ofType: PumpEventStored.self,
                     onContext: self.context,
                     predicate: NSPredicate.duplicateInLastHour(event.date),
@@ -259,8 +259,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
     }
 
-    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
@@ -418,8 +418,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
-    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPumpHistoryNotYetUploadedToHealth() async throws -> [PumpHistoryEvent] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
@@ -460,8 +460,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
-    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPumpHistoryNotYetUploadedToTidepool() async throws -> [PumpHistoryEvent] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,

+ 51 - 50
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -10,16 +10,16 @@ protocol TempTargetsObserver {
 protocol TempTargetsStorage {
     func storeTempTarget(tempTarget: TempTarget) async
     func saveTempTargetsToStorage(_ targets: [TempTarget])
-    func fetchForTempTargetPresets() async -> [NSManagedObjectID]
-    func fetchScheduledTempTargets() async -> [NSManagedObjectID]
-    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
+    func fetchForTempTargetPresets() async throws -> [NSManagedObjectID]
+    func fetchScheduledTempTargets() async throws -> [NSManagedObjectID]
+    func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID]
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
-    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
     func syncDate() -> Date
     func recent() -> [TempTarget]
-    func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getTempTargetsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getTempTargetRunsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func presets() -> [TempTarget]
     func current() -> TempTarget?
     func existsTempTarget(with date: Date) async -> Bool
@@ -38,8 +38,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         injectServices(resolver)
     }
 
-    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -56,8 +56,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     }
 
     /// Returns the NSManagedObjectID of the Temp Target Presets
-    func fetchForTempTargetPresets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchForTempTargetPresets() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.allTempTargetPresets,
@@ -72,10 +72,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         }
     }
 
-    func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
+    func fetchScheduledTempTargets() async throws -> [NSManagedObjectID] {
         let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: scheduledTempTargets,
@@ -90,10 +90,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         }
     }
 
-    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
+    func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: predicate,
@@ -110,44 +110,45 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     }
 
     func storeTempTarget(tempTarget: TempTarget) async {
-        var presetCount = -1
-        if tempTarget.isPreset == true {
-            let presets = await fetchForTempTargetPresets()
-            presetCount = presets.count
-        }
-
-        await backgroundContext.perform {
-            let newTempTarget = TempTargetStored(context: self.backgroundContext)
-            newTempTarget.date = tempTarget.createdAt
-            newTempTarget.id = UUID()
-            newTempTarget.enabled = tempTarget.enabled ?? false
-            newTempTarget.duration = tempTarget.duration as NSDecimalNumber
-            newTempTarget.isUploadedToNS = false
-            newTempTarget.name = tempTarget.name
-            newTempTarget.target = NSDecimalNumber(decimal: tempTarget.targetTop ?? 0)
-            newTempTarget.isPreset = tempTarget.isPreset ?? false
-
-            // Nullify half basal target to ensure the latest HBT is used via OpenAPS Manager when sending TT data to oref
-            newTempTarget.halfBasalTarget = nil
-
-            if let halfBasalTarget = tempTarget.halfBasalTarget,
-               halfBasalTarget != self.settingsManager.preferences.halfBasalExerciseTarget
-            {
-                newTempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalTarget)
+        do {
+            var presetCount = -1
+            if tempTarget.isPreset == true {
+                let presets = try await fetchForTempTargetPresets()
+                presetCount = presets.count
             }
 
-            if tempTarget.isPreset == true, presetCount > -1 {
-                newTempTarget.orderPosition = Int16(presetCount + 1)
-            }
+            try await backgroundContext.perform {
+                let newTempTarget = TempTargetStored(context: self.backgroundContext)
+                newTempTarget.date = tempTarget.createdAt
+                newTempTarget.id = UUID()
+                newTempTarget.enabled = tempTarget.enabled ?? false
+                newTempTarget.duration = tempTarget.duration as NSDecimalNumber
+                newTempTarget.isUploadedToNS = false
+                newTempTarget.name = tempTarget.name
+                newTempTarget.target = NSDecimalNumber(decimal: tempTarget.targetTop ?? 0)
+                newTempTarget.isPreset = tempTarget.isPreset ?? false
+
+                // Nullify half basal target to ensure the latest HBT is used via OpenAPS Manager when sending TT data to oref
+                newTempTarget.halfBasalTarget = nil
+
+                if let halfBasalTarget = tempTarget.halfBasalTarget,
+                   halfBasalTarget != self.settingsManager.preferences.halfBasalExerciseTarget
+                {
+                    newTempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalTarget)
+                }
+
+                if tempTarget.isPreset == true, presetCount > -1 {
+                    newTempTarget.orderPosition = Int16(presetCount + 1)
+                }
 
-            do {
                 guard self.backgroundContext.hasChanges else { return }
                 try self.backgroundContext.save()
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target to Core Data with error: \(error.userInfo)"
-                )
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to store temp target: \(error.localizedDescription)"
+            )
         }
     }
 
@@ -242,8 +243,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return last
     }
 
-    func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getTempTargetsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout, // TODO: create adjustment predicate (OR+TT)
@@ -277,8 +278,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         }
     }
 
-    func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getTempTargetRunsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate(

+ 9 - 2
Trio/Sources/Application/AppDelegate.swift

@@ -23,7 +23,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
             let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
 
             Task {
-                await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                try await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
                 completionHandler(.newData)
             }
         } catch {
@@ -40,7 +40,14 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         let token = tokenParts.joined()
 
         Task {
-            await TrioRemoteControl.shared.handleAPNSChanges(deviceToken: token)
+            do {
+                try await TrioRemoteControl.shared.handleAPNSChanges(deviceToken: token)
+            } catch {
+                debug(
+                    .remoteControl,
+                    "\(DebuggingIdentifiers.failed) failed to register for remote notifications: \(error.localizedDescription)"
+                )
+            }
         }
     }
 

+ 125 - 84
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -29,19 +29,22 @@ extension Adjustments.StateModel {
     // MARK: - Disable Overrides
 
     /// Disables all active Overrides, optionally creating a run entry.
-    @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
-        // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
+    @MainActor func disableAllActiveOverrides(
+        except overrideID: NSManagedObjectID? = nil,
+        createOverrideRunEntry: Bool
+    ) async {
+        do {
+            // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
 
-        await viewContext.perform {
-            do {
+            try await viewContext.perform {
                 // Fetch the existing OverrideStored objects from the context
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
                 guard !results.isEmpty else { return }
 
-                // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
+                // Check if we also need to create a corresponding OverrideRunStored entry
                 if createOverrideRunEntry {
                     // Use the first override to create a new OverrideRunStored entry
                     if let canceledOverride = results.first {
@@ -50,8 +53,9 @@ extension Adjustments.StateModel {
                         newOverrideRunStored.name = canceledOverride.name
                         newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                         newOverrideRunStored.endDate = Date()
-                        newOverrideRunStored
-                            .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                        newOverrideRunStored.target = NSDecimalNumber(
+                            decimal: self.overrideStorage.calculateTarget(override: canceledOverride)
+                        )
                         newOverrideRunStored.override = canceledOverride
                         newOverrideRunStored.isUploadedToNS = false
                     }
@@ -67,11 +71,12 @@ extension Adjustments.StateModel {
                     try self.viewContext.save()
                     self.updateLatestOverrideConfiguration()
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides: \(error.localizedDescription)"
-                )
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error.localizedDescription)"
+            )
         }
     }
 
@@ -79,74 +84,89 @@ extension Adjustments.StateModel {
 
     /// Saves a custom Override and activates it.
     func saveCustomOverride() async {
-        let override = Override(
-            name: overrideName,
-            enabled: true,
-            date: Date(),
-            duration: overrideDuration,
-            indefinite: indefinite,
-            percentage: overridePercentage,
-            smbIsOff: smbIsOff,
-            isPreset: isPreset,
-            id: id,
-            overrideTarget: shouldOverrideTarget,
-            target: target,
-            advancedSettings: advancedSettings,
-            isfAndCr: isfAndCr,
-            isf: isf,
-            cr: cr,
-            smbIsScheduledOff: smbIsScheduledOff,
-            start: start,
-            end: end,
-            smbMinutes: smbMinutes,
-            uamMinutes: uamMinutes
-        )
-
-        // First disable all Overrides
-        await disableAllActiveOverrides(createOverrideRunEntry: true)
-
-        // Then save and activate a new custom Override
-        await overrideStorage.storeOverride(override: override)
-
-        // Reset State variables
-        await resetStateVariables()
-
-        // Update View
-        updateLatestOverrideConfiguration()
+        do {
+            let override = Override(
+                name: overrideName,
+                enabled: true,
+                date: Date(),
+                duration: overrideDuration,
+                indefinite: indefinite,
+                percentage: overridePercentage,
+                smbIsOff: smbIsOff,
+                isPreset: isPreset,
+                id: id,
+                overrideTarget: shouldOverrideTarget,
+                target: target,
+                advancedSettings: advancedSettings,
+                isfAndCr: isfAndCr,
+                isf: isf,
+                cr: cr,
+                smbIsScheduledOff: smbIsScheduledOff,
+                start: start,
+                end: end,
+                smbMinutes: smbMinutes,
+                uamMinutes: uamMinutes
+            )
+
+            // First disable all Overrides
+            await disableAllActiveOverrides(createOverrideRunEntry: true)
+
+            // Then save and activate a new custom Override
+            try await overrideStorage.storeOverride(override: override)
+
+            // Reset State variables
+            await resetStateVariables()
+
+            // Update View
+            updateLatestOverrideConfiguration()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to save custom override: \(error.localizedDescription)"
+            )
+        }
     }
 
     /// Saves an Override Preset without activating it.
     /// `enabled` has to be false
     /// `isPreset` has to be true
     func saveOverridePreset() async {
-        let preset = Override(
-            name: overrideName,
-            enabled: false,
-            date: Date(),
-            duration: overrideDuration,
-            indefinite: indefinite,
-            percentage: overridePercentage,
-            smbIsOff: smbIsOff,
-            isPreset: true,
-            id: id,
-            overrideTarget: shouldOverrideTarget,
-            target: target,
-            advancedSettings: advancedSettings,
-            isfAndCr: isfAndCr,
-            isf: isf,
-            cr: cr,
-            smbIsScheduledOff: smbIsScheduledOff,
-            start: start,
-            end: end,
-            smbMinutes: smbMinutes,
-            uamMinutes: uamMinutes
-        )
-
-        async let storeOverride: () = overrideStorage.storeOverride(override: preset)
-        async let resetState: () = resetStateVariables()
-        _ = await (storeOverride, resetState)
-        setupOverridePresetsArray()
-        await nightscoutManager.uploadProfiles()
+        do {
+            let preset = Override(
+                name: overrideName,
+                enabled: false,
+                date: Date(),
+                duration: overrideDuration,
+                indefinite: indefinite,
+                percentage: overridePercentage,
+                smbIsOff: smbIsOff,
+                isPreset: true,
+                id: id,
+                overrideTarget: shouldOverrideTarget,
+                target: target,
+                advancedSettings: advancedSettings,
+                isfAndCr: isfAndCr,
+                isf: isf,
+                cr: cr,
+                smbIsScheduledOff: smbIsScheduledOff,
+                start: start,
+                end: end,
+                smbMinutes: smbMinutes,
+                uamMinutes: uamMinutes
+            )
+
+            async let storeOverride: () = overrideStorage.storeOverride(override: preset)
+            async let resetState: () = resetStateVariables()
+            _ = try await (storeOverride, resetState)
+
+            setupOverridePresetsArray()
+            try await nightscoutManager.uploadProfiles()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to save override preset: \(error.localizedDescription)"
+            )
+        }
     }
 
     // MARK: - Override Preset Management
@@ -154,8 +174,15 @@ extension Adjustments.StateModel {
     /// Sets up the array of Override Presets for UI display.
     func setupOverridePresetsArray() {
         Task {
-            let ids = await overrideStorage.fetchForOverridePresets()
-            await updateOverridePresetsArray(with: ids)
+            do {
+                let ids = try await overrideStorage.fetchForOverridePresets()
+                await updateOverridePresetsArray(with: ids)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to setup override presets: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
@@ -175,9 +202,16 @@ extension Adjustments.StateModel {
 
     /// Deletes an Override Preset and updates the view.
     func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
-        await overrideStorage.deleteOverridePreset(objectID)
-        setupOverridePresetsArray()
-        await nightscoutManager.uploadProfiles()
+        do {
+            await overrideStorage.deleteOverridePreset(objectID)
+            setupOverridePresetsArray()
+            try await nightscoutManager.uploadProfiles()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to delete override preset: \(error.localizedDescription)"
+            )
+        }
     }
 
     // MARK: - Update Latest Override Configuration
@@ -188,13 +222,20 @@ extension Adjustments.StateModel {
     /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
     func updateLatestOverrideConfiguration() {
         Task { [weak self] in
-            guard let self = self else { return }
+            do {
+                guard let self = self else { return }
 
-            let id = await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
+                let id = try await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
 
-            // execute sequentially instead of concurrently
-            await self.updateLatestOverrideConfigurationOfState(from: id)
-            await self.setCurrentOverride(from: id)
+                // execute sequentially instead of concurrently
+                await self.updateLatestOverrideConfigurationOfState(from: id)
+                await self.setCurrentOverride(from: id)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to update override configuration: \(error.localizedDescription)"
+                )
+            }
         }
     }
 

+ 55 - 35
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -11,7 +11,7 @@ extension Adjustments.StateModel {
     /// This also needs to be called when we cancel an Temp Target via the Home View to update the State of the Button for this case
     func updateLatestTempTargetConfiguration() {
         Task {
-            let id = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
+            let id = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
             async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
             async let setTempTarget: () = setCurrentTempTarget(from: id)
             _ = await (updateState, setTempTarget)
@@ -58,13 +58,20 @@ extension Adjustments.StateModel {
 
     /// Sets up Temp Targets using fetch and update functions.
     func setupTempTargets(
-        fetchFunction: @escaping () async -> [NSManagedObjectID],
+        fetchFunction: @escaping () async throws -> [NSManagedObjectID],
         updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
     ) {
         Task {
-            let ids = await fetchFunction()
-            let tempTargetObjects = await fetchTempTargetObjects(for: ids)
-            await updateFunction(tempTargetObjects)
+            do {
+                let ids = try await fetchFunction()
+                let tempTargetObjects = await fetchTempTargetObjects(for: ids)
+                await updateFunction(tempTargetObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to setup temp targets: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
@@ -83,7 +90,7 @@ extension Adjustments.StateModel {
     /// Sets up the Temp Target presets array for the view.
     func setupTempTargetPresetsArray() {
         setupTempTargets(
-            fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
+            fetchFunction: { try await self.tempTargetStorage.fetchForTempTargetPresets() },
             updateFunction: { tempTargets in
                 self.tempTargetPresets = tempTargets
             }
@@ -93,7 +100,7 @@ extension Adjustments.StateModel {
     /// Sets up the scheduled Temp Targets array for the view.
     func setupScheduledTempTargetsArray() {
         setupTempTargets(
-            fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
+            fetchFunction: { try await self.tempTargetStorage.fetchScheduledTempTargets() },
             updateFunction: { tempTargets in
                 self.scheduledTempTargets = tempTargets
             }
@@ -146,25 +153,35 @@ extension Adjustments.StateModel {
 
     /// Enables a scheduled Temp Target for a specific date.
     func enableScheduledTempTarget(for date: Date) async {
-        let ids = await tempTargetStorage.fetchScheduledTempTarget(for: date)
-        guard let firstID = ids.first else {
-            debugPrint("No Temp Target found for the specified date.")
-            return
-        }
-        await setCurrentTempTarget(from: ids)
-
-        await MainActor.run {
-            do {
-                if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
-                    tempTarget.enabled = true
-                    try viewContext.save()
-                    isTempTargetEnabled = true
+        do {
+            let ids = try await tempTargetStorage.fetchScheduledTempTarget(for: date)
+            guard let firstID = ids.first else {
+                debug(.default, "No Temp Target found for the specified date.")
+                return
+            }
+            await setCurrentTempTarget(from: ids)
+
+            try await MainActor.run {
+                guard let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored else {
+                    throw NSError(
+                        domain: "TempTarget",
+                        code: -1,
+                        userInfo: [NSLocalizedDescriptionKey: "Failed to find temp target"]
+                    )
                 }
-            } catch {
-                debugPrint("Failed to enable the Temp Target: \(error.localizedDescription)")
+
+                tempTarget.enabled = true
+                try viewContext.save()
+                isTempTargetEnabled = true
             }
+
+            setupScheduledTempTargetsArray()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to enable scheduled temp target: \(error.localizedDescription)"
+            )
         }
-        setupScheduledTempTargetsArray()
     }
 
     /// Waits until a target date before proceeding.
@@ -259,12 +276,15 @@ extension Adjustments.StateModel {
     }
 
     /// Disables all active Temp Targets.
-    @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
-        // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
-        let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
+    @MainActor func disableAllActiveTempTargets(
+        except id: NSManagedObjectID? = nil,
+        createTempTargetRunEntry: Bool
+    ) async {
+        do {
+            // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
+            let ids = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
-        await viewContext.perform {
-            do {
+            try await viewContext.perform {
                 // Fetch the existing TempTargetStored objects from the context
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
@@ -273,7 +293,7 @@ extension Adjustments.StateModel {
                 // If there are no results, return early
                 guard !results.isEmpty else { return }
 
-                // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
+                // Check if we also need to create a corresponding TempTargetRunStored entry
                 if createTempTargetRunEntry {
                     // Use the first temp target to create a new TempTargetRunStored entry
                     if let canceledTempTarget = results.first {
@@ -282,8 +302,7 @@ extension Adjustments.StateModel {
                         newTempTargetRunStored.name = canceledTempTarget.name
                         newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                         newTempTargetRunStored.endDate = Date()
-                        newTempTargetRunStored
-                            .target = canceledTempTarget.target ?? 0
+                        newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                         newTempTargetRunStored.tempTarget = canceledTempTarget
                         newTempTargetRunStored.isUploadedToNS = false
                     }
@@ -303,11 +322,12 @@ extension Adjustments.StateModel {
                     // Update the storage
                     self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
-                )
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error.localizedDescription)"
+            )
         }
     }
 

+ 1 - 1
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -174,7 +174,7 @@ extension Adjustments {
                 guard viewContext.hasChanges else { return }
                 try viewContext.save()
                 setupOverridePresetsArray()
-                Task { await nightscoutManager.uploadProfiles() }
+                Task { try await nightscoutManager.uploadProfiles() }
             } catch {
                 debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Presets order")
             }

+ 1 - 1
Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift

@@ -518,7 +518,7 @@ struct EditOverrideForm: View {
                         guard moc.hasChanges else { return }
                         try moc.save()
                         Task {
-                            await state.nightscoutManager.uploadProfiles()
+                            try await state.nightscoutManager.uploadProfiles()
                         }
                         // Disable previous active Override
                         if let currentActiveOverride = state.currentActiveOverride {

+ 1 - 1
Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -67,7 +67,7 @@ extension AlgorithmAdvancedSettings {
 
                         Task.detached(priority: .low) {
                             debug(.nightscout, "Attempting to upload DIA to Nightscout")
-                            await self.nightscout.uploadProfiles()
+                            try await self.nightscout.uploadProfiles()
                         }
                     } receiveValue: {}
                     .store(in: &lifetime)

+ 11 - 4
Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -41,10 +41,17 @@ extension AutosensSettings {
 
         private func setupDeterminationsArray() {
             Task {
-                let ids = await determinationStorage.fetchLastDeterminationObjectID(
-                    predicate: NSPredicate.enactedDetermination
-                )
-                await updateDeterminationsArray(with: ids)
+                do {
+                    let ids = try await determinationStorage.fetchLastDeterminationObjectID(
+                        predicate: NSPredicate.enactedDetermination
+                    )
+                    await updateDeterminationsArray(with: ids)
+                } catch {
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) Error fetching determination IDs: \(error.localizedDescription)"
+                    )
+                }
             }
         }
 

+ 1 - 1
Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -95,7 +95,7 @@ extension BasalProfileEditor {
 
                         Task.detached(priority: .low) {
                             debug(.nightscout, "Attempting to upload basal rates to Nightscout")
-                            await self.nightscout.uploadProfiles()
+                            try await self.nightscout.uploadProfiles()
                         }
                     case .failure:
                         // Handle the error, show error message

+ 1 - 1
Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift

@@ -71,7 +71,7 @@ extension CarbRatioEditor {
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
             Task.detached(priority: .low) {
                 debug(.nightscout, "Attempting to upload CRs to Nightscout")
-                await self.nightscout.uploadProfiles()
+                try await self.nightscout.uploadProfiles()
             }
         }
 

+ 3 - 3
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -304,7 +304,7 @@ extension DataTable {
                     newNote: newNote
                 )
 
-                await createNewEntries(
+                try await createNewEntries(
                     originalDate: newDate,
                     newCarbs: newCarbs,
                     newFat: newFat,
@@ -322,7 +322,7 @@ extension DataTable {
             newFat: Decimal,
             newProtein: Decimal,
             newNote: String
-        ) async {
+        ) async throws {
             let newEntry = CarbsEntry(
                 id: UUID().uuidString,
                 createdAt: Date(),
@@ -337,7 +337,7 @@ extension DataTable {
             )
 
             // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
-            await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+            try await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
         }
 
         /// Deletes the old carb/ FPU entries and creates new ones with updated values

+ 13 - 6
Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupBatteryArray() {
         Task {
-            let ids = await self.fetchBattery()
-            let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateBatteryArray(with: batteryObjects)
+            do {
+                let ids = try await self.fetchBattery()
+                let batteryObjects: [OpenAPS_Battery] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateBatteryArray(with: batteryObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up battery array: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    private func fetchBattery() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchBattery() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OpenAPS_Battery.self,
             onContext: batteryFetchContext,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -21,7 +29,6 @@ extension Home.StateModel {
 
         return await batteryFetchContext.perform {
             guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
-
             return fetchedResults.map(\.objectID)
         }
     }

+ 20 - 10
Trio/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift

@@ -4,14 +4,19 @@ import Foundation
 extension Home.StateModel {
     func setupCarbsArray() {
         Task {
-            let ids = await self.fetchCarbs()
-            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateCarbsArray(with: carbObjects)
+            do {
+                let ids = try await self.fetchCarbs()
+                let carbObjects: [CarbEntryStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateCarbsArray(with: carbObjects)
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error fetching carb objects: \(error) in \(#file):\(#line)")
+            }
         }
     }
 
-    private func fetchCarbs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchCarbs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: carbsFetchContext,
             predicate: NSPredicate.carbsForChart,
@@ -33,14 +38,19 @@ extension Home.StateModel {
 
     func setupFPUsArray() {
         Task {
-            let ids = await self.fetchFPUs()
-            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateFPUsArray(with: fpuObjects)
+            do {
+                let ids = try await self.fetchFPUs()
+                let fpuObjects: [CarbEntryStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateFPUsArray(with: fpuObjects)
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error fetching FPU objects: \(error) in \(#file):\(#line)")
+            }
         }
     }
 
-    private func fetchFPUs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchFPUs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: fpuFetchContext,
             predicate: NSPredicate.fpusForChart,

+ 20 - 14
Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift

@@ -4,28 +4,34 @@ import Foundation
 extension Home.StateModel {
     func setupDeterminationsArray() {
         Task {
-            // Get the NSManagedObjectIDs
-            async let enactedObjectIds = determinationStorage
-                .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
-            async let enactedAndNonEnactedObjectIds = fetchCobAndIob()
+            do {
+                // Get the NSManagedObjectIDs
+                async let enactedObjectIds = determinationStorage
+                    .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
+                async let enactedAndNonEnactedObjectIds = fetchCobAndIob()
 
-            let enactedIDs = await enactedObjectIds
-            let enactedAndNonEnactedIds = await enactedAndNonEnactedObjectIds
+                let enactedIDs = try await enactedObjectIds
+                let enactedAndNonEnactedIds = try await enactedAndNonEnactedObjectIds
 
-            // Get the NSManagedObjects and return them on the Main Thread
-            await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
-            await updateDeterminationsArray(with: enactedAndNonEnactedIds, keyPath: \.enactedAndNonEnactedDeterminations)
+                // Get the NSManagedObjects and return them on the Main Thread
+                try await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
+                try await updateDeterminationsArray(with: enactedAndNonEnactedIds, keyPath: \.enactedAndNonEnactedDeterminations)
 
-            await updateForecastData()
+                await updateForecastData()
+            } catch let error as CoreDataError {
+                debug(.default, "Core Data error in setupDeterminationsArray: \(error.localizedDescription)")
+            } catch {
+                debug(.default, "Unexpected error in setupDeterminationsArray: \(error.localizedDescription)")
+            }
         }
     }
 
     @MainActor private func updateDeterminationsArray(
         with IDs: [NSManagedObjectID],
         keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) async {
+    ) async throws {
         // Fetch the objects off the main thread
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
+        let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
             .getNSManagedObject(with: IDs, context: viewContext)
 
         // Update the array on the main thread
@@ -33,8 +39,8 @@ extension Home.StateModel {
     }
 
     // Custom fetch to more efficiently filter only for cob and iob
-    private func fetchCobAndIob() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchCobAndIob() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: determinationFetchContext,
             predicate: NSPredicate.determinationsForCobIobCharts,

+ 19 - 10
Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -6,18 +6,27 @@ extension Home.StateModel {
     func preprocessForecastData() async -> [(
         id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]
     )] {
-        // Get the Determination ID on the main context
-        guard let determination = await viewContext.perform({
-            self.enactedAndNonEnactedDeterminations.first
-        }) else {
+        do {
+            // Get the Determination ID on the main context
+            guard let determination = await viewContext.perform({
+                self.enactedAndNonEnactedDeterminations.first
+            }) else {
+                debug(.default, "No determination found for forecast preprocessing")
+                return []
+            }
+
+            // Fetch complete forecast hierarchy with prefetched values
+            return try await determinationStorage.fetchForecastHierarchy(
+                for: determination.objectID,
+                in: taskContext
+            )
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to preprocess forecast data: \(error.localizedDescription)"
+            )
             return []
         }
-
-        // Fetch complete forecast hierarchy with prefetched values
-        return await determinationStorage.fetchForecastHierarchy(
-            for: determination.objectID,
-            in: taskContext
-        )
     }
 
     // Update forecast data and UI on the main thread

+ 13 - 5
Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupGlucoseArray() {
         Task {
-            let ids = await self.fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateGlucoseArray(with: glucoseObjects)
+            do {
+                let ids = try await self.fetchGlucose()
+                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateGlucoseArray(with: glucoseObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,

+ 24 - 13
Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift

@@ -5,14 +5,21 @@ extension Home.StateModel {
     // Setup Overrides
     func setupOverrides() {
         Task {
-            let ids = await self.fetchOverrides()
-            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideArray(with: overrideObjects)
+            do {
+                let ids = try await self.fetchOverrides()
+                let overrideObjects: [OverrideStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateOverrideArray(with: overrideObjects)
+            } catch let error as CoreDataError {
+                debug(.default, "Core Data error in setupOverrides: \(error.localizedDescription)")
+            } catch {
+                debug(.default, "Unexpected error in setupOverrides: \(error.localizedDescription)")
+            }
         }
     }
 
-    private func fetchOverrides() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchOverrides() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: overrideFetchContext,
             predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
@@ -22,7 +29,6 @@ extension Home.StateModel {
 
         return await overrideFetchContext.perform {
             guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
             return fetchedResults.map(\.objectID)
         }
     }
@@ -41,16 +47,22 @@ extension Home.StateModel {
     // Setup expired Overrides
     func setupOverrideRunStored() {
         Task {
-            let ids = await self.fetchOverrideRunStored()
-            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideRunStoredArray(with: overrideRunObjects)
+            do {
+                let ids = try await self.fetchOverrideRunStored()
+                let overrideRunObjects: [OverrideRunStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateOverrideRunStoredArray(with: overrideRunObjects)
+            } catch let error as CoreDataError {
+                debug(.default, "Core Data error in setupOverrideRunStored: \(error.localizedDescription)")
+            } catch {
+                debug(.default, "Unexpected error in setupOverrideRunStored: \(error.localizedDescription)")
+            }
         }
     }
 
-    private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
+    private func fetchOverrideRunStored() async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
             onContext: overrideFetchContext,
             predicate: predicate,
@@ -60,7 +72,6 @@ extension Home.StateModel {
 
         return await overrideFetchContext.perform {
             guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
-
             return fetchedResults.map(\.objectID)
         }
     }

+ 24 - 9
Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupInsulinArray() {
         Task {
-            let ids = await self.fetchInsulin()
-            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateInsulinArray(with: insulinObjects)
+            do {
+                let ids = try await self.fetchInsulin()
+                let insulinObjects: [PumpEventStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateInsulinArray(with: insulinObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up insulin array: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    private func fetchInsulin() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchInsulin() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.pumpHistoryLast24h,
@@ -48,13 +56,20 @@ extension Home.StateModel {
     // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
     func setupLastBolus() {
         Task {
-            guard let id = await self.fetchLastBolus() else { return }
-            await updateLastBolus(with: id)
+            do {
+                guard let id = try await self.fetchLastBolus() else { return }
+                await updateLastBolus(with: id)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    func fetchLastBolus() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.lastPumpBolus,

+ 26 - 12
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -4,15 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupTempTargetsStored() {
         Task {
-            let ids = await self.fetchTempTargets()
-            let tempTargetObjects: [TempTargetStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateTempTargetsArray(with: tempTargetObjects)
+            do {
+                let ids = try await self.fetchTempTargets()
+                let tempTargetObjects: [TempTargetStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateTempTargetsArray(with: tempTargetObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up tempTargetStored: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    private func fetchTempTargets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchTempTargets() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: tempTargetFetchContext,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -33,16 +40,23 @@ extension Home.StateModel {
     // Setup expired TempTargets
     func setupTempTargetsRunStored() {
         Task {
-            let ids = await self.fetchTempTargetRunStored()
-            let tempTargetRunObjects: [TempTargetRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+            do {
+                let ids = try await self.fetchTempTargetRunStored()
+                let tempTargetRunObjects: [TempTargetRunStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up temp targetsRunStored: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    private func fetchTempTargetRunStored() async -> [NSManagedObjectID] {
+    private func fetchTempTargetRunStored() async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
             onContext: tempTargetFetchContext,
             predicate: predicate,

+ 1 - 1
Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -86,7 +86,7 @@ extension ISFEditor {
 
             Task.detached(priority: .low) {
                 debug(.nightscout, "Attempting to upload ISF to Nightscout")
-                await self.nightscout.uploadProfiles()
+                try await self.nightscout.uploadProfiles()
             }
         }
 

+ 18 - 13
Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -75,7 +75,7 @@ extension NightscoutConfig {
                     if enabled {
                         debug(.nightscout, "Upload has been enabled by the user.")
                         Task {
-                            await self.nightscoutManager.uploadProfiles()
+                            try await self.nightscoutManager.uploadProfiles()
                         }
                     } else {
                         debug(.nightscout, "Upload has been disabled by the user.")
@@ -344,20 +344,25 @@ extension NightscoutConfig {
             let glucose = await nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
 
             if glucose.isNotEmpty {
-                await MainActor.run {
-                    self.backfilling = false
-                }
-
-                await glucoseStorage.storeGlucose(glucose)
+                do {
+                    try await glucoseStorage.storeGlucose(glucose)
 
-                Task.detached {
-                    await self.healthKitManager.uploadGlucose()
+                    Task.detached {
+                        await self.healthKitManager.uploadGlucose()
+                    }
+                } catch let error as CoreDataError {
+                    debug(.nightscout, "Core Data error while storing backfilled glucose: \(error.localizedDescription)")
+                    message = "Error: \(error.localizedDescription)"
+                } catch {
+                    debug(.nightscout, "Unexpected error while storing backfilled glucose: \(error.localizedDescription)")
+                    message = "Error: \(error.localizedDescription)"
                 }
             } else {
-                await MainActor.run {
-                    self.backfilling = false
-                    debug(.nightscout, "No glucose values found or fetched to backfill.")
-                }
+                debug(.nightscout, "No glucose values found or fetched to backfill.")
+            }
+
+            await MainActor.run {
+                self.backfilling = false
             }
         }
 
@@ -384,7 +389,7 @@ extension NightscoutConfig {
 
                         Task.detached(priority: .low) {
                             debug(.nightscout, "Attempting to upload DIA to Nightscout after import review")
-                            await self.nightscoutManager.uploadProfiles()
+                            try await self.nightscoutManager.uploadProfiles()
                         }
                     } receiveValue: {}
                     .store(in: &lifetime)

+ 15 - 0
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -16,6 +16,8 @@ extension NightscoutConfig {
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
+        @State var backfillAlert: Alert?
+        @State var isBackfillAlertPresented = false
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
@@ -132,6 +134,16 @@ extension NightscoutConfig {
                                 Button {
                                     Task {
                                         await state.backfillGlucose()
+                                        if !state.message.isEmpty && state.message.hasPrefix("Error:") {
+                                            DispatchQueue.main.async {
+                                                backfillAlert = Alert(
+                                                    title: Text("Backfill Failed"),
+                                                    message: Text(state.message),
+                                                    dismissButton: .default(Text("OK"))
+                                                )
+                                                isBackfillAlertPresented = true
+                                            }
+                                        }
                                     }
                                 } label: {
                                     Text("Backfill Glucose")
@@ -194,6 +206,9 @@ extension NightscoutConfig {
             .alert(isPresented: $isImportAlertPresented) {
                 importAlert ?? Alert(title: Text("Unknown Error"))
             }
+            .alert(isPresented: $isBackfillAlertPresented) {
+                backfillAlert ?? Alert(title: Text("Unknown Error"))
+            }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
         }

+ 30 - 26
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -46,35 +46,39 @@ extension Stat {
         }
 
         private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
-            let predicate: NSPredicate
-
-            switch duration {
-            case .Day:
-                predicate = NSPredicate.glucoseForStatsDay
-            case .Week:
-                predicate = NSPredicate.glucoseForStatsWeek
-            case .Today:
-                predicate = NSPredicate.glucoseForStatsToday
-            case .Month:
-                predicate = NSPredicate.glucoseForStatsMonth
-            case .Total:
-                predicate = NSPredicate.glucoseForStatsTotal
-            }
+            do {
+                let predicate: NSPredicate
 
-            let results = await CoreDataStack.shared.fetchEntitiesAsync(
-                ofType: GlucoseStored.self,
-                onContext: context,
-                predicate: predicate,
-                key: "date",
-                ascending: false,
-                batchSize: 100,
-                propertiesToFetch: ["glucose", "objectID"]
-            )
+                switch duration {
+                case .Day:
+                    predicate = NSPredicate.glucoseForStatsDay
+                case .Week:
+                    predicate = NSPredicate.glucoseForStatsWeek
+                case .Today:
+                    predicate = NSPredicate.glucoseForStatsToday
+                case .Month:
+                    predicate = NSPredicate.glucoseForStatsMonth
+                case .Total:
+                    predicate = NSPredicate.glucoseForStatsTotal
+                }
 
-            return await context.perform {
-                guard let fetchedResults = results as? [[String: Any]] else { return [] }
+                let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+                    ofType: GlucoseStored.self,
+                    onContext: context,
+                    predicate: predicate,
+                    key: "date",
+                    ascending: false,
+                    batchSize: 100,
+                    propertiesToFetch: ["glucose", "objectID"]
+                )
 
-                return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+                return await context.perform {
+                    guard let fetchedResults = results as? [[String: Any]] else { return [] }
+                    return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)")
+                return []
             }
         }
 

+ 1 - 1
Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -84,7 +84,7 @@ extension TargetsEditor {
 
             Task.detached(priority: .low) {
                 debug(.nightscout, "Attempting to upload targets to Nightscout")
-                await self.nightscout.uploadProfiles()
+                try await self.nightscout.uploadProfiles()
             }
         }
 

+ 140 - 108
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -170,32 +170,39 @@ extension Treatments {
         private func setupBolusStateConcurrently() {
             debug(.bolusState, "setupBolusStateConcurrently fired")
             Task {
-                await withTaskGroup(of: Void.self) { group in
-                    group.addTask {
-                        self.setupGlucoseArray()
-                    }
-                    group.addTask {
-                        self.setupDeterminationsAndForecasts()
-                    }
-                    group.addTask {
-                        await self.setupSettings()
-                    }
-                    group.addTask {
-                        self.registerObservers()
-                    }
-
-                    if self.waitForSuggestionInitial {
+                do {
+                    try await withThrowingTaskGroup(of: Void.self) { group in
+                        group.addTask {
+                            self.setupGlucoseArray()
+                        }
+                        group.addTask {
+                            self.setupDeterminationsAndForecasts()
+                        }
                         group.addTask {
-                            let isDetermineBasalSuccessful = await self.apsManager.determineBasal()
-                            if !isDetermineBasalSuccessful {
-                                await MainActor.run {
-                                    self.waitForSuggestion = false
-                                    self.insulinRequired = 0
-                                    self.insulinRecommended = 0
+                            await self.setupSettings()
+                        }
+                        group.addTask {
+                            self.registerObservers()
+                        }
+
+                        if self.waitForSuggestionInitial {
+                            group.addTask {
+                                let isDetermineBasalSuccessful = try await self.apsManager.determineBasal()
+                                if !isDetermineBasalSuccessful {
+                                    await MainActor.run {
+                                        self.waitForSuggestion = false
+                                        self.insulinRequired = 0
+                                        self.insulinRecommended = 0
+                                    }
                                 }
                             }
                         }
+
+                        // Wait for all tasks to complete
+                        try await group.waitForAll()
                     }
+                } catch let error as NSError {
+                    debug(.default, "Failed to setup bolus state concurrently: \(error.localizedDescription)")
                 }
             }
         }
@@ -206,7 +213,7 @@ extension Treatments {
         ///   - `apsManager.bolusProgress` is a `CurrentValueSubject<Decimal?, Never>`.
         ///   - When a bolus starts, this subject emits `0` (or a fraction like `0.1, 0.5, etc.`).
         ///   - When the bolus finishes, the subject is typically set to `nil`.
-        ///   - This treats ANY non-nil value as “bolus in progress.”
+        ///   - This treats ANY non-nil value as "bolus in progress."
         ///
         private func subscribeToBolusProgress() {
             bolusProgressCancellable = apsManager.bolusProgress
@@ -537,34 +544,39 @@ extension Treatments {
         // MARK: - Carbs
 
         func saveMeal() async {
-            guard carbs > 0 || fat > 0 || protein > 0 else { return }
+            do {
+                guard carbs > 0 || fat > 0 || protein > 0 else { return }
 
-            await MainActor.run {
-                self.carbs = min(self.carbs, self.maxCarbs)
-                self.fat = min(self.fat, self.maxFat)
-                self.protein = min(self.protein, self.maxProtein)
-                self.id_ = UUID().uuidString
-            }
+                await MainActor.run {
+                    self.carbs = min(self.carbs, self.maxCarbs)
+                    self.fat = min(self.fat, self.maxFat)
+                    self.protein = min(self.protein, self.maxProtein)
+                    self.id_ = UUID().uuidString
+                }
 
-            let carbsToStore = [CarbsEntry(
-                id: id_,
-                createdAt: now,
-                actualDate: date,
-                carbs: carbs,
-                fat: fat,
-                protein: protein,
-                note: note,
-                enteredBy: CarbsEntry.local,
-                isFPU: false,
-                fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
-            )]
-            await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
-
-            if carbs > 0 || fat > 0 || protein > 0 {
-                // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
-                if amount <= 0 {
-                    await apsManager.determineBasalSync()
+                let carbsToStore = [CarbsEntry(
+                    id: id_,
+                    createdAt: now,
+                    actualDate: date,
+                    carbs: carbs,
+                    fat: fat,
+                    protein: protein,
+                    note: note,
+                    enteredBy: CarbsEntry.local,
+                    isFPU: false,
+                    fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
+                )]
+
+                try await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
+
+                if carbs > 0 || fat > 0 || protein > 0 {
+                    // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
+                    if amount <= 0 {
+                        await apsManager.determineBasalSync()
+                    }
                 }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save meal with error: \(error.localizedDescription)")
             }
         }
 
@@ -673,14 +685,22 @@ extension Treatments.StateModel {
     // Glucose
     private func setupGlucoseArray() {
         Task {
-            let ids = await self.fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateGlucoseArray(with: glucoseObjects)
+            do {
+                let ids = try await self.fetchGlucose()
+                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateGlucoseArray(with: glucoseObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error.localizedDescription)"
+                )
+            }
         }
     }
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
@@ -708,71 +728,83 @@ extension Treatments.StateModel {
 
     // Determinations
     private func setupDeterminationsArray() async {
-        // Fetch object IDs on a background thread
-        let fetchedObjectIDs = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
+        do {
+            let fetchedObjectIDs = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
+            )
 
-        // Update determinationObjectIDs on the main thread
-        await MainActor.run {
-            determinationObjectIDs = fetchedObjectIDs
-        }
+            await MainActor.run {
+                determinationObjectIDs = fetchedObjectIDs
+            }
 
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
 
-        updateDeterminationsArray(with: determinationObjects)
+            updateDeterminationsArray(with: determinationObjects)
+        } catch let error as CoreDataError {
+            debug(.default, "Core Data error: \(error.localizedDescription)")
+        } catch {
+            debug(.default, "Unexpected error: \(error.localizedDescription)")
+        }
     }
 
     private func mapForecastsForChart() async -> Determination? {
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
-
-        return await determinationFetchContext.perform {
-            guard let determinationObject = determinationObjects.first else {
-                return nil
-            }
+        do {
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
 
-            let eventualBG = determinationObject.eventualBG?.intValue
-
-            let forecastsSet = determinationObject.forecasts ?? []
-            let predictions = Predictions(
-                iob: forecastsSet.extractValues(for: "iob"),
-                zt: forecastsSet.extractValues(for: "zt"),
-                cob: forecastsSet.extractValues(for: "cob"),
-                uam: forecastsSet.extractValues(for: "uam")
-            )
+            return await determinationFetchContext.perform {
+                guard let determinationObject = determinationObjects.first else {
+                    return nil
+                }
 
-            return Determination(
-                id: UUID(),
-                reason: "",
-                units: 0,
-                insulinReq: 0,
-                eventualBG: eventualBG,
-                sensitivityRatio: 0,
-                rate: 0,
-                duration: 0,
-                iob: 0,
-                cob: 0,
-                predictions: predictions.isEmpty ? nil : predictions,
-                carbsReq: 0,
-                temp: nil,
-                bg: 0,
-                reservoir: 0,
-                isf: 0,
-                tdd: 0,
-                insulin: nil,
-                current_target: 0,
-                insulinForManualBolus: 0,
-                manualBolusErrorString: 0,
-                minDelta: 0,
-                expectedDelta: 0,
-                minGuardBG: 0,
-                minPredBG: 0,
-                threshold: 0,
-                carbRatio: 0,
-                received: false
+                let eventualBG = determinationObject.eventualBG?.intValue
+
+                let forecastsSet = determinationObject.forecasts ?? []
+                let predictions = Predictions(
+                    iob: forecastsSet.extractValues(for: "iob"),
+                    zt: forecastsSet.extractValues(for: "zt"),
+                    cob: forecastsSet.extractValues(for: "cob"),
+                    uam: forecastsSet.extractValues(for: "uam")
+                )
+
+                return Determination(
+                    id: UUID(),
+                    reason: "",
+                    units: 0,
+                    insulinReq: 0,
+                    eventualBG: eventualBG,
+                    sensitivityRatio: 0,
+                    rate: 0,
+                    duration: 0,
+                    iob: 0,
+                    cob: 0,
+                    predictions: predictions.isEmpty ? nil : predictions,
+                    carbsReq: 0,
+                    temp: nil,
+                    bg: 0,
+                    reservoir: 0,
+                    isf: 0,
+                    tdd: 0,
+                    insulin: nil,
+                    current_target: 0,
+                    insulinForManualBolus: 0,
+                    manualBolusErrorString: 0,
+                    minDelta: 0,
+                    expectedDelta: 0,
+                    minGuardBG: 0,
+                    minPredBG: 0,
+                    threshold: 0,
+                    carbRatio: 0,
+                    received: false
+                )
+            }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error.localizedDescription)"
             )
+            return nil
         }
     }
 

+ 98 - 65
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -191,8 +191,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
 
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
@@ -266,64 +266,73 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         carbs: Decimal,
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool
-    ) async -> CalculationInput {
-        // Get settings
-        let settings = await getSettings()
-
-        // Get max bolus
-        let maxBolus = await getPumpSettings().maxBolus
-
-        // Get current profile values
-        let currentBasal = await getCurrentSettingValue(for: .basal)
-        let currentCarbRatio = await getCurrentSettingValue(for: .carbRatio)
-        let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
-        let currentISF = await getCurrentSettingValue(for: .isf)
-
-        // Fetch glucose data
-        let glucoseIds = await fetchGlucose()
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(
-            with: glucoseIds,
-            context: glucoseFetchContext
-        )
-        let glucoseVars = await glucoseFetchContext.perform {
-            self.updateGlucoseVariables(with: glucoseObjects)
-        }
+    ) async throws -> CalculationInput {
+        do {
+            // Get settings
+            let settings = await getSettings()
+
+            // Get max bolus
+            let maxBolus = await getPumpSettings().maxBolus
+
+            // Get current profile values
+            let currentBasal = await getCurrentSettingValue(for: .basal)
+            let currentCarbRatio = await getCurrentSettingValue(for: .carbRatio)
+            let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
+            let currentISF = await getCurrentSettingValue(for: .isf)
+
+            // Fetch glucose data
+            let glucoseIds = try await fetchGlucose()
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared.getNSManagedObject(
+                with: glucoseIds,
+                context: glucoseFetchContext
+            )
+            let glucoseVars = await glucoseFetchContext.perform {
+                self.updateGlucoseVariables(with: glucoseObjects)
+            }
 
-        // Fetch determination data
-        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared.getNSManagedObject(
-            with: determinationIds,
-            context: determinationFetchContext
-        )
-        let bolusVars = await determinationFetchContext.perform {
-            self.updateBolusCalculatorVariables(
-                with: determinationObjects,
-                currentBGTarget: currentBGTarget,
-                currentISF: currentISF,
-                currentCarbRatio: currentCarbRatio,
-                currentBasal: currentBasal
+            // Fetch determination data
+            let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
             )
-        }
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared.getNSManagedObject(
+                with: determinationIds,
+                context: determinationFetchContext
+            )
+            let bolusVars = await determinationFetchContext.perform {
+                self.updateBolusCalculatorVariables(
+                    with: determinationObjects,
+                    currentBGTarget: currentBGTarget,
+                    currentISF: currentISF,
+                    currentCarbRatio: currentCarbRatio,
+                    currentBasal: currentBasal
+                )
+            }
 
-        return CalculationInput(
-            carbs: carbs,
-            currentBG: glucoseVars.currentBG,
-            deltaBG: glucoseVars.deltaBG,
-            target: bolusVars.target,
-            isf: bolusVars.isf,
-            carbRatio: bolusVars.carbRatio,
-            iob: bolusVars.iob,
-            cob: bolusVars.cob,
-            useFattyMealCorrectionFactor: useFattyMealCorrection,
-            fattyMealFactor: settings.fattyMealFactor,
-            useSuperBolus: useSuperBolus,
-            sweetMealFactor: settings.sweetMealFactor,
-            basal: bolusVars.basal,
-            fraction: settings.fraction,
-            maxBolus: maxBolus
-        )
+            return CalculationInput(
+                carbs: carbs,
+                currentBG: glucoseVars.currentBG,
+                deltaBG: glucoseVars.deltaBG,
+                target: bolusVars.target,
+                isf: bolusVars.isf,
+                carbRatio: bolusVars.carbRatio,
+                iob: bolusVars.iob,
+                cob: bolusVars.cob,
+                useFattyMealCorrectionFactor: useFattyMealCorrection,
+                fattyMealFactor: settings.fattyMealFactor,
+                useSuperBolus: useSuperBolus,
+                sweetMealFactor: settings.sweetMealFactor,
+                basal: bolusVars.basal,
+                fraction: settings.fraction,
+                maxBolus: maxBolus
+            )
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error preparing calculation input: \(error.localizedDescription)"
+            )
+            // Return default values in case of error
+            throw error
+        }
     }
 
     /// Calculates the recommended insulin dose based on various parameters
@@ -401,14 +410,38 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     ///   - useFattyMealCorrection: Whether to apply fatty meal correction
     ///   - useSuperBolus: Whether to use super bolus calculation
     /// - Returns: CalculationResult containing the calculated insulin dose and details
-    func handleBolusCalculation(carbs: Decimal, useFattyMealCorrection: Bool, useSuperBolus: Bool) async -> CalculationResult {
-        let input = await prepareCalculationInput(
-            carbs: carbs,
-            useFattyMealCorrection: useFattyMealCorrection,
-            useSuperBolus: useSuperBolus
-        )
-        let result = await calculateInsulin(input: input)
-        return result
+    func handleBolusCalculation(
+        carbs: Decimal,
+        useFattyMealCorrection: Bool,
+        useSuperBolus: Bool
+    ) async -> CalculationResult {
+        do {
+            let input = try await prepareCalculationInput(
+                carbs: carbs,
+                useFattyMealCorrection: useFattyMealCorrection,
+                useSuperBolus: useSuperBolus
+            )
+            let result = await calculateInsulin(input: input)
+            return result
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error in bolus calculation: \(error.localizedDescription)"
+            )
+            // Return safe default values
+            return CalculationResult(
+                insulinCalculated: 0,
+                wholeCalc: 0,
+                correctionInsulin: 0,
+                iobInsulinReduction: 0,
+                superBolusInsulin: 0,
+                targetDifference: 0,
+                targetDifferenceInsulin: 0,
+                fifteenMinutesInsulin: 0,
+                wholeCob: 0,
+                wholeCobInsulin: 0
+            )
+        }
     }
 }
 

+ 13 - 13
Trio/Sources/Services/Calendar/CalendarManager.swift

@@ -175,8 +175,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         EKEventStore().calendars(for: .event).map(\.title)
     }
 
-    private func getLastDetermination() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func getLastDetermination() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -193,8 +193,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         }
     }
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -211,19 +211,19 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     }
 
     @MainActor func createEvent() async {
-        guard settingsManager.settings.useCalendar, let calendar = currentCalendar,
-              let determinationId = await getLastDetermination() else { return }
+        do {
+            guard settingsManager.settings.useCalendar, let calendar = currentCalendar,
+                  let determinationId = try await getLastDetermination() else { return }
 
-        // Ignore the update if the determinationId is the same as it was at last update
-        if determinationId == previousDeterminationId {
-            return
-        }
+            // Ignore the update if the determinationId is the same as it was at last update
+            if determinationId == previousDeterminationId {
+                return
+            }
 
-        let glucoseIds = await fetchGlucose()
+            let glucoseIds = try await fetchGlucose()
 
-        deleteAllEvents(in: calendar)
+            deleteAllEvents(in: calendar)
 
-        do {
             guard let determinationObject = try viewContext.existingObject(with: determinationId) as? OrefDetermination
             else { return }
 

+ 69 - 64
Trio/Sources/Services/ContactImage/ContactImageManager.swift

@@ -88,8 +88,8 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
 
     // MARK: - Core Data Fetches
 
-    private func fetchlastDetermination() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchlastDetermination() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: backgroundContext,
             predicate: NSPredicate(format: "deliverAt >= %@", Date.halfHourAgo as NSDate), // fetches enacted and suggested
@@ -105,8 +105,8 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
         }
     }
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor20MinAgo,
@@ -180,72 +180,77 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     /// and updates the `state` object, which represents the current contact trick state.
     /// - Important: This function must be called on the main actor to ensure thread safety. Otherwise, we would need to ensure thread safety by either using an actor or a perform closure
     @MainActor func updateContactImageState() async {
-        // Get NSManagedObjectIDs on backgroundContext
-        let glucoseValuesIds = await fetchGlucose()
-        let determinationIds = await fetchlastDetermination()
-
-        // Get NSManagedObjects on MainActor
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationIds, context: viewContext)
-        let lastDetermination = determinationObjects.last
-
-        if let firstGlucoseValue = glucoseObjects.first {
-            let value = settingsManager.settings.units == .mgdL
-                ? Decimal(firstGlucoseValue.glucose)
-                : Decimal(firstGlucoseValue.glucose).asMmolL
-
-            state.glucose = Formatter.glucoseFormatter(for: units).string(from: value as NSNumber)
-            state.trend = firstGlucoseValue.directionEnum?.symbol
-
-            let delta = glucoseObjects.count >= 2
-                ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseObjects.dropFirst().first?.glucose ?? 0)
-                : 0
-            let deltaConverted = settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
-            state.delta = deltaFormatter.string(from: deltaConverted as NSNumber)
-        }
+        do {
+            // Get NSManagedObjectIDs on backgroundContext
+            let glucoseValuesIds = try await fetchGlucose()
+            let determinationIds = try await fetchlastDetermination()
+
+            // Get NSManagedObjects on MainActor
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationIds, context: viewContext)
+            let lastDetermination = determinationObjects.last
+
+            if let firstGlucoseValue = glucoseObjects.first {
+                let value = settingsManager.settings.units == .mgdL
+                    ? Decimal(firstGlucoseValue.glucose)
+                    : Decimal(firstGlucoseValue.glucose).asMmolL
+
+                state.glucose = Formatter.glucoseFormatter(for: units).string(from: value as NSNumber)
+                state.trend = firstGlucoseValue.directionEnum?.symbol
+
+                let delta = glucoseObjects.count >= 2
+                    ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseObjects.dropFirst().first?.glucose ?? 0)
+                    : 0
+                let deltaConverted = settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
+                state.delta = deltaFormatter.string(from: deltaConverted as NSNumber)
+            }
 
-        state.lastLoopDate = lastDetermination?.timestamp
+            state.lastLoopDate = lastDetermination?.timestamp
 
-        let iobValue = lastDetermination?.iob as? Decimal ?? 0.0
-        state.iob = iobValue
-        state.iobText = Formatter.decimalFormatterWithOneFractionDigit.string(from: iobValue as NSNumber)
+            let iobValue = lastDetermination?.iob as? Decimal ?? 0.0
+            state.iob = iobValue
+            state.iobText = Formatter.decimalFormatterWithOneFractionDigit.string(from: iobValue as NSNumber)
 
-        // we need to do it complex and unelegant, otherwise unwrapping and parsing of cob results in 0
-        if let cobValue = lastDetermination?.cob {
-            state.cob = Decimal(cobValue)
-            state.cobText = Formatter.integerFormatter.string(from: Int(cobValue) as NSNumber)
+            // we need to do it complex and unelegant, otherwise unwrapping and parsing of cob results in 0
+            if let cobValue = lastDetermination?.cob {
+                state.cob = Decimal(cobValue)
+                state.cobText = Formatter.integerFormatter.string(from: Int(cobValue) as NSNumber)
 
-        } else {
-            state.cob = 0
-            state.cobText = "0"
-        }
+            } else {
+                state.cob = 0
+                state.cobText = "0"
+            }
 
-        if let eventualBG = settingsManager.settings.units == .mgdL ? lastDetermination?
-            .eventualBG : lastDetermination?
-            .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
-        {
-            let eventualBGAsString = Formatter.decimalFormatterWithOneFractionDigit.string(from: eventualBG)
-            state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
-        }
+            if let eventualBG = settingsManager.settings.units == .mgdL ? lastDetermination?
+                .eventualBG : lastDetermination?
+                .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
+            {
+                let eventualBGAsString = Formatter.decimalFormatterWithOneFractionDigit.string(from: eventualBG)
+                state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
+            }
 
-        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
-        let hardCodedLow = Decimal(55)
-        let hardCodedHigh = Decimal(220)
-        let isDynamicColorScheme = settingsManager.settings.glucoseColorScheme == .dynamicColor
-        let highGlucoseColorValue = isDynamicColorScheme ? hardCodedHigh : settingsManager.settings.highGlucose
-        let lowGlucoseColorValue = isDynamicColorScheme ? hardCodedLow : settingsManager.settings.lowGlucose
-
-        state.highGlucoseColorValue = units == .mgdL ? highGlucoseColorValue : highGlucoseColorValue.asMmolL
-        state.lowGlucoseColorValue = units == .mgdL ? lowGlucoseColorValue : lowGlucoseColorValue.asMmolL
-        state
-            .targetGlucose = await getCurrentGlucoseTarget() ??
-            (settingsManager.settings.units == .mgdL ? Decimal(100) : 100.asMmolL)
-        state.glucoseColorScheme = settingsManager.settings.glucoseColorScheme
-
-        // Notify delegate about state update on main thread
-        await MainActor.run {
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = settingsManager.settings.glucoseColorScheme == .dynamicColor
+            let highGlucoseColorValue = isDynamicColorScheme ? hardCodedHigh : settingsManager.settings.highGlucose
+            let lowGlucoseColorValue = isDynamicColorScheme ? hardCodedLow : settingsManager.settings.lowGlucose
+
+            state.highGlucoseColorValue = units == .mgdL ? highGlucoseColorValue : highGlucoseColorValue.asMmolL
+            state.lowGlucoseColorValue = units == .mgdL ? lowGlucoseColorValue : lowGlucoseColorValue.asMmolL
+            state
+                .targetGlucose = await getCurrentGlucoseTarget() ??
+                (settingsManager.settings.units == .mgdL ? Decimal(100) : 100.asMmolL)
+            state.glucoseColorScheme = settingsManager.settings.glucoseColorScheme
+
+            // Notify delegate about state update on main thread
+            await MainActor.run {
+                delegate?.contactImageManagerDidUpdateState(state)
+            }
+        } catch {
+            // Still notify delegate with current state, even if there was an error
             delegate?.contactImageManagerDidUpdateState(state)
         }
     }

+ 103 - 74
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -156,8 +156,18 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Glucose Upload
 
     func uploadGlucose() async {
-        await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
-        await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
+        do {
+            let glucose = try await glucoseStorage.getGlucoseNotYetUploadedToHealth()
+            await uploadGlucose(glucose)
+
+            let manualGlucose = try await glucoseStorage.getManualGlucoseNotYetUploadedToHealth()
+            await uploadGlucose(manualGlucose)
+        } catch {
+            debug(
+                .service,
+                "\(DebuggingIdentifiers.failed) Error fetching glucose for health upload: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadGlucose(_ glucose: [BloodGlucose]) async {
@@ -228,7 +238,15 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Carbs Upload
 
     func uploadCarbs() async {
-        await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToHealth())
+        do {
+            let carbs = try await carbsStorage.getCarbsNotYetUploadedToHealth()
+            await uploadCarbs(carbs)
+        } catch {
+            debug(
+                .service,
+                "\(DebuggingIdentifiers.failed) Error fetching carbs for health upload: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadCarbs(_ carbs: [CarbsEntry]) async {
@@ -341,7 +359,15 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Insulin Upload
 
     func uploadInsulin() async {
-        await uploadInsulin(pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth())
+        do {
+            let events = try await pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth()
+            await uploadInsulin(events)
+        } catch {
+            debug(
+                .service,
+                "\(DebuggingIdentifiers.failed) Error fetching insulin events for health upload: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadInsulin(_ insulinEvents: [PumpHistoryEvent]) async {
@@ -350,87 +376,90 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
               insulinEvents.isNotEmpty else { return }
 
-        // Fetch existing temp basal entries from Core Data for the last 24 hours
-        let fetchedInsulinEntries = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: backgroundContext,
-            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
-                NSPredicate.pumpHistoryLast24h,
-                NSPredicate(format: "tempBasal != nil")
-            ]),
-            key: "timestamp",
-            ascending: true,
-            batchSize: 50
-        )
-
-        var insulinSamples: [HKQuantitySample] = []
+        do {
+            let fetchedInsulinEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: PumpEventStored.self,
+                onContext: backgroundContext,
+                predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                    NSPredicate.pumpHistoryLast24h,
+                    NSPredicate(format: "tempBasal != nil")
+                ]),
+                key: "timestamp",
+                ascending: true,
+                batchSize: 50
+            )
 
-        await backgroundContext.perform {
-            guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else { return }
-
-            for event in insulinEvents {
-                switch event.type {
-                case .bolus:
-                    // For bolus events, create a HealthKit sample directly
-                    if let sample = self.createSample(for: event, sampleType: sampleType) {
-                        debug(.service, "Created HealthKit sample for bolus entry: \(sample)")
-                        insulinSamples.append(sample)
-                    }
-                case .tempBasal:
-                    // For temp basal events, process them and adjust overlapping durations if necessary
-                    guard let duration = event.duration, let amount = event.amount else { continue }
-
-                    let value = (Decimal(duration) / 60.0) * amount
-                    let valueRounded = self.deviceDataManager?.pumpManager?
-                        .roundToSupportedBolusVolume(units: Double(value)) ?? Double(value)
-
-                    // Use binary search for efficient lookup of matching entry
-                    if let matchingIndex = self.binarySearch(entries: existingTempBasalEntries, timestamp: event.timestamp) {
-                        let predecessorIndex = matchingIndex - 1
-
-                        if predecessorIndex >= 0 {
-                            let predecessorEntry = existingTempBasalEntries[predecessorIndex]
-
-                            if let adjustedSample = self.processPredecessorEntry(
-                                predecessorEntry,
-                                nextEventTimestamp: event.timestamp,
-                                sampleType: sampleType
-                            ) {
-                                insulinSamples.append(adjustedSample)
-                            }
-                        }
+            var insulinSamples: [HKQuantitySample] = []
 
-                        let newEvent = PumpHistoryEvent(
-                            id: event.id,
-                            type: .tempBasal,
-                            timestamp: event.timestamp,
-                            amount: Decimal(valueRounded),
-                            duration: event.duration
-                        )
+            await backgroundContext.perform {
+                guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else { return }
 
-                        if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
-                            debug(.service, "Created HealthKit sample for initial temp basal entry: \(sample)")
+                for event in insulinEvents {
+                    switch event.type {
+                    case .bolus:
+                        // For bolus events, create a HealthKit sample directly
+                        if let sample = self.createSample(for: event, sampleType: sampleType) {
+                            debug(.service, "Created HealthKit sample for bolus entry: \(sample)")
                             insulinSamples.append(sample)
                         }
-                    }
+                    case .tempBasal:
+                        // For temp basal events, process them and adjust overlapping durations if necessary
+                        guard let duration = event.duration, let amount = event.amount else { continue }
+
+                        let value = (Decimal(duration) / 60.0) * amount
+                        let valueRounded = self.deviceDataManager?.pumpManager?
+                            .roundToSupportedBolusVolume(units: Double(value)) ?? Double(value)
+
+                        // Use binary search for efficient lookup of matching entry
+                        if let matchingIndex = self.binarySearch(entries: existingTempBasalEntries, timestamp: event.timestamp) {
+                            let predecessorIndex = matchingIndex - 1
+
+                            if predecessorIndex >= 0 {
+                                let predecessorEntry = existingTempBasalEntries[predecessorIndex]
+
+                                if let adjustedSample = self.processPredecessorEntry(
+                                    predecessorEntry,
+                                    nextEventTimestamp: event.timestamp,
+                                    sampleType: sampleType
+                                ) {
+                                    insulinSamples.append(adjustedSample)
+                                }
+                            }
+
+                            let newEvent = PumpHistoryEvent(
+                                id: event.id,
+                                type: .tempBasal,
+                                timestamp: event.timestamp,
+                                amount: Decimal(valueRounded),
+                                duration: event.duration
+                            )
+
+                            if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
+                                debug(.service, "Created HealthKit sample for initial temp basal entry: \(sample)")
+                                insulinSamples.append(sample)
+                            }
+                        }
 
-                default:
-                    break
+                    default:
+                        break
+                    }
                 }
             }
-        }
 
-        do {
-            guard insulinSamples.isNotEmpty else {
-                debug(.service, "No insulin samples available for upload.")
-                return
-            }
+            do {
+                guard insulinSamples.isNotEmpty else {
+                    debug(.service, "No insulin samples available for upload.")
+                    return
+                }
 
-            try await healthKitStore.save(insulinSamples)
-            debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
-            await updateInsulinAsUploaded(insulinEvents)
+                try await healthKitStore.save(insulinSamples)
+                debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
+                await updateInsulinAsUploaded(insulinEvents)
+            } catch {
+                debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
+            }
         } catch {
-            debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
+            debug(.service, "\(DebuggingIdentifiers.failed) Error fetching temp basal entries: \(error.localizedDescription)")
         }
     }
 

+ 6 - 6
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -4,8 +4,8 @@ import Foundation
 
 @available(iOS 16.2, *)
 extension LiveActivityBridge {
-    func fetchAndMapGlucose() async -> [GlucoseData] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchAndMapGlucose() async throws -> [GlucoseData] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForSixHoursAgo,
@@ -25,8 +25,8 @@ extension LiveActivityBridge {
         }
     }
 
-    func fetchAndMapDetermination() async -> DeterminationData? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchAndMapDetermination() async throws -> DeterminationData? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -53,8 +53,8 @@ extension LiveActivityBridge {
         }
     }
 
-    func fetchAndMapOverride() async -> OverrideData? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchAndMapOverride() async throws -> OverrideData? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,

+ 3 - 3
Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -135,7 +135,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
     private func cobOrIobDidUpdate() {
         Task { @MainActor in
-            self.determination = await fetchAndMapDetermination()
+            self.determination = try await fetchAndMapDetermination()
             if let determination = determination {
                 await self.updateContentState(determination)
             }
@@ -144,7 +144,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
     private func overridesDidUpdate() {
         Task { @MainActor in
-            self.override = await fetchAndMapOverride()
+            self.override = try await fetchAndMapOverride()
             if let determination = determination {
                 await self.updateContentState(determination)
             }
@@ -205,7 +205,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
     private func setupGlucoseArray() {
         Task { @MainActor in
-            self.glucoseFromPersistence = await fetchAndMapGlucose()
+            self.glucoseFromPersistence = try await fetchAndMapGlucose()
             glucoseDidUpdate(glucoseFromPersistence ?? [])
         }
     }

+ 64 - 18
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -12,14 +12,14 @@ protocol NightscoutManager: GlucoseSource {
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteManualGlucose(withID id: String) async
-    func uploadDeviceStatus() async
+    func uploadDeviceStatus() async throws
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadOverrides() async
     func uploadTempTargets() async
     func uploadManualGlucose() async
-    func uploadProfiles() async
+    func uploadProfiles() async throws
     func uploadNoteTreatment(note: String) async
     func importSettings() async -> ScheduledNightscoutProfile?
     var cgmURL: URL? { get }
@@ -107,7 +107,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             async let lastEnactedDeterminationID = determinationStorage
                 .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
 
-            self.lastEnactedDetermination = await determinationStorage
+            self.lastEnactedDetermination = try await determinationStorage
                 .getOrefDeterminationNotYetUploadedToNightscout(lastEnactedDeterminationID)
         }
     }
@@ -246,7 +246,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .sink { [weak self] in
                 guard let self = self else { return }
                 Task {
-                    await self.uploadDeviceStatus()
+                    do {
+                        try await self.uploadDeviceStatus()
+                    } catch {
+                        debug(.nightscout, "\(DebuggingIdentifiers.failed) failed to upload device status")
+                    }
                 }
             }
             .store(in: &subscriptions)
@@ -504,7 +508,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     ///
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Returns: Nothing.
-    func uploadDeviceStatus() async {
+    func uploadDeviceStatus() async throws {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             return
@@ -522,7 +526,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         async let fetchedPumpStatus = storage.retrieveAsync(OpenAPS.Monitor.status, as: PumpStatus.self)
 
-        var (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
+        var (fetchedEnactedDetermination, fetchedSuggestedDetermination) = try await (
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(enactedDeterminationID),
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(suggestedDeterminationID)
         )
@@ -691,7 +695,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadProfiles() async {
+    func uploadProfiles() async throws {
         if isUploadEnabled {
             do {
                 guard let sensitivities = await storage.retrieveAsync(
@@ -793,7 +797,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
                 let deviceToken = UserDefaults.standard.string(forKey: "deviceToken") ?? ""
                 let isAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
-                let presetOverrides = await overridesStorage.getPresetOverridesForNightscout()
+                let presetOverrides = try await overridesStorage.getPresetOverridesForNightscout()
                 let teamID = Bundle.main.object(forInfoDictionaryKey: "TeamID") as? String ?? ""
 
                 let profileStore = NightscoutProfileStore(
@@ -845,31 +849,73 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func uploadGlucose() async {
-        await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToNightscout())
-        await uploadNonCoreDataTreatments(glucoseStorage.getCGMStateNotYetUploadedToNightscout())
+        do {
+            try await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToNightscout())
+            try await uploadNonCoreDataTreatments(glucoseStorage.getCGMStateNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload glucose with error: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadManualGlucose() async {
-        await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
+        do {
+            try await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload manual glucose with error: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadPumpHistory() async {
-        await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
+        do {
+            try await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload pump history with error: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadCarbs() async {
-        await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
-        await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
+        do {
+            try await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
+            try await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload carbs with error: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadOverrides() async {
-        await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
-        await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
+        do {
+            try await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
+            try await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload overrides with error: \(error.localizedDescription)"
+            )
+        }
     }
 
     func uploadTempTargets() async {
-        await uploadTempTargets(await tempTargetsStorage.getTempTargetsNotYetUploadedToNightscout())
-        await uploadTempTargetRuns(await tempTargetsStorage.getTempTargetRunsNotYetUploadedToNightscout())
+        do {
+            try await uploadTempTargets(await tempTargetsStorage.getTempTargetsNotYetUploadedToNightscout())
+            try await uploadTempTargetRuns(await tempTargetsStorage.getTempTargetRunsNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload temp targets with error: \(error.localizedDescription)"
+            )
+        }
     }
 
     private func uploadGlucose(_ glucose: [BloodGlucose]) async {

+ 116 - 96
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -187,7 +187,11 @@ extension BaseTidepoolManager: ServiceDelegate {
 /// Carb Upload and Deletion Functionality
 extension BaseTidepoolManager {
     func uploadCarbs() async {
-        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
+        do {
+            try uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
+        } catch {
+            debug(.service, "\(DebuggingIdentifiers.failed) Failed to upload carbs with error: \(error.localizedDescription)")
+        }
     }
 
     func uploadCarbs(_ carbs: [CarbsEntry]) {
@@ -274,115 +278,127 @@ extension BaseTidepoolManager {
 /// Insulin Upload and Deletion Functionality
 extension BaseTidepoolManager {
     func uploadInsulin() async {
-        await uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
+        do {
+            let events = try await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool()
+            await uploadDose(events)
+        } catch {
+            debug(.service, "Error fetching pump history: \(error.localizedDescription)")
+        }
     }
 
     func uploadDose(_ events: [PumpHistoryEvent]) async {
         guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
-        // Fetch all temp basal entries from Core Data for the last 24 hours
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: backgroundContext,
-            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
-                NSPredicate.pumpHistoryLast24h,
-                NSPredicate(format: "tempBasal != nil")
-            ]),
-            key: "timestamp",
-            ascending: true,
-            batchSize: 50
-        )
-
-        // Ensure that the processing happens within the background context for thread safety
-        await backgroundContext.perform {
-            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
-
-            let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
-                var result = result
-                switch event.type {
-                case .tempBasal:
-                    result
-                        .append(contentsOf: self.processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries))
-                case .bolus:
-                    let bolusDoseEntry = DoseEntry(
-                        type: .bolus,
-                        startDate: event.timestamp,
-                        endDate: event.timestamp,
-                        value: Double(event.amount!),
-                        unit: .units,
-                        deliveredUnits: nil,
-                        syncIdentifier: event.id,
-                        scheduledBasalRate: nil,
-                        insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
-                        automatic: event.isSMB ?? true,
-                        manuallyEntered: event.isExternal ?? false
-                    )
-                    result.append(bolusDoseEntry)
-                default:
-                    break
+        do {
+            // Fetch all temp basal entries from Core Data for the last 24 hours
+            let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: PumpEventStored.self,
+                onContext: backgroundContext,
+                predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                    NSPredicate.pumpHistoryLast24h,
+                    NSPredicate(format: "tempBasal != nil")
+                ]),
+                key: "timestamp",
+                ascending: true,
+                batchSize: 50
+            )
+
+            // Ensure that the processing happens within the background context for thread safety
+            await backgroundContext.perform {
+                guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
+
+                let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
+                    var result = result
+                    switch event.type {
+                    case .tempBasal:
+                        result
+                            .append(
+                                contentsOf: self
+                                    .processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries)
+                            )
+                    case .bolus:
+                        let bolusDoseEntry = DoseEntry(
+                            type: .bolus,
+                            startDate: event.timestamp,
+                            endDate: event.timestamp,
+                            value: Double(event.amount!),
+                            unit: .units,
+                            deliveredUnits: nil,
+                            syncIdentifier: event.id,
+                            scheduledBasalRate: nil,
+                            insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                            automatic: event.isSMB ?? true,
+                            manuallyEntered: event.isExternal ?? false
+                        )
+                        result.append(bolusDoseEntry)
+                    default:
+                        break
+                    }
+                    return result
                 }
-                return result
-            }
 
-            debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
+                debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
+
+                let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
+                    if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
+                        let dose: DoseEntry? = switch pumpEventType {
+                        case .suspend:
+                            DoseEntry(suspendDate: event.timestamp, automatic: true)
+                        case .resume:
+                            DoseEntry(resumeDate: event.timestamp, automatic: true)
+                        default:
+                            nil
+                        }
 
-            let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
-                if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
-                    let dose: DoseEntry? = switch pumpEventType {
-                    case .suspend:
-                        DoseEntry(suspendDate: event.timestamp, automatic: true)
-                    case .resume:
-                        DoseEntry(resumeDate: event.timestamp, automatic: true)
-                    default:
-                        nil
+                        return PersistedPumpEvent(
+                            date: event.timestamp,
+                            persistedDate: event.timestamp,
+                            dose: dose,
+                            isUploaded: true,
+                            objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
+                            raw: event.id.data(using: .utf8),
+                            title: event.note,
+                            type: pumpEventType
+                        )
+                    } else {
+                        return nil
                     }
-
-                    return PersistedPumpEvent(
-                        date: event.timestamp,
-                        persistedDate: event.timestamp,
-                        dose: dose,
-                        isUploaded: true,
-                        objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
-                        raw: event.id.data(using: .utf8),
-                        title: event.note,
-                        type: pumpEventType
-                    )
-                } else {
-                    return nil
                 }
-            }
 
-            self.processQueue.async {
-                tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
-                    switch result {
-                    case let .failure(error):
-                        debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
-                    case .success:
-                        debug(.nightscout, "Success synchronizing dose data. Upload to Tidepool complete.")
-                        Task {
-                            let insulinEvents = events.filter {
-                                $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+                self.processQueue.async {
+                    tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
+                        switch result {
+                        case let .failure(error):
+                            debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
+                        case .success:
+                            debug(.nightscout, "Success synchronizing dose data. Upload to Tidepool complete.")
+                            Task {
+                                let insulinEvents = events.filter {
+                                    $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+                                }
+                                await self.updateInsulinAsUploaded(insulinEvents)
                             }
-                            await self.updateInsulinAsUploaded(insulinEvents)
                         }
                     }
-                }
 
-                tidepoolService.uploadPumpEventData(pumpEvents) { result in
-                    switch result {
-                    case let .failure(error):
-                        debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
-                    case .success:
-                        debug(.nightscout, "Success synchronizing pump events data. Upload to Tidepool complete.")
-                        Task {
-                            let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
-                            let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
-
-                            await self.updateInsulinAsUploaded(pumpEvents)
+                    tidepoolService.uploadPumpEventData(pumpEvents) { result in
+                        switch result {
+                        case let .failure(error):
+                            debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
+                        case .success:
+                            debug(.nightscout, "Success synchronizing pump events data. Upload to Tidepool complete.")
+                            Task {
+                                let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
+                                let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
+
+                                await self.updateInsulinAsUploaded(pumpEvents)
+                            }
                         }
                     }
                 }
             }
+        } catch {
+            debug(.service, "Error fetching temp basal entries: \(error.localizedDescription)")
         }
     }
 
@@ -573,11 +589,15 @@ extension BaseTidepoolManager {
 /// Glucose Upload Functionality
 extension BaseTidepoolManager {
     func uploadGlucose() async {
-        uploadGlucose(await glucoseStorage.getGlucoseNotYetUploadedToTidepool())
-        uploadGlucose(
-            await glucoseStorage
-                .getManualGlucoseNotYetUploadedToTidepool()
-        )
+        do {
+            let glucose = try await glucoseStorage.getGlucoseNotYetUploadedToTidepool()
+            uploadGlucose(glucose)
+
+            let manualGlucose = try await glucoseStorage.getManualGlucoseNotYetUploadedToTidepool()
+            uploadGlucose(manualGlucose)
+        } catch {
+            debug(.service, "Error fetching glucose data: \(error.localizedDescription)")
+        }
     }
 
     func uploadGlucose(_ glucose: [StoredGlucoseSample]) {

+ 2 - 2
Trio/Sources/Services/RemoteControl/TrioRemoteControl+APNS.swift

@@ -1,7 +1,7 @@
 import Foundation
 
 extension TrioRemoteControl {
-    internal func handleAPNSChanges(deviceToken: String?) async {
+    internal func handleAPNSChanges(deviceToken: String?) async throws {
         let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
         let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
 
@@ -21,7 +21,7 @@ extension TrioRemoteControl {
         }
 
         if shouldUploadProfiles {
-            await nightscoutManager.uploadProfiles()
+            try await nightscoutManager.uploadProfiles()
         } else {
             debug(.remoteControl, "No changes detected in device token or APNS environment.")
         }

+ 8 - 7
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -1,7 +1,7 @@
 import Foundation
 
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ pushMessage: PushMessage) async {
+    internal func handleBolusCommand(_ pushMessage: PushMessage) async throws {
         guard let bolusAmount = pushMessage.bolusAmount else {
             await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
             return
@@ -18,7 +18,7 @@ extension TrioRemoteControl {
         }
 
         let maxIOB = settings.preferences.maxIOB
-        let currentIOB = await fetchCurrentIOB()
+        let currentIOB = try await fetchCurrentIOB()
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
@@ -27,7 +27,8 @@ extension TrioRemoteControl {
             return
         }
 
-        let totalRecentBolusAmount = await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+        let totalRecentBolusAmount =
+            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
 
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
             await logError(
@@ -55,10 +56,10 @@ extension TrioRemoteControl {
         )
     }
 
-    private func fetchCurrentIOB() async -> Decimal {
+    private func fetchCurrentIOB() async throws -> Decimal {
         let predicate = NSPredicate.predicateFor30MinAgoForDetermination
 
-        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
+        let determinations = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: pumpHistoryFetchContext,
             predicate: predicate,
@@ -79,14 +80,14 @@ extension TrioRemoteControl {
         return iob
     }
 
-    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
+    private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
         let predicate = NSPredicate(
             format: "type == %@ AND timestamp > %@",
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
         )
 
-        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             predicate: predicate,

+ 2 - 2
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -1,7 +1,7 @@
 import Foundation
 
 extension TrioRemoteControl {
-    func handleMealCommand(_ pushMessage: PushMessage) async {
+    func handleMealCommand(_ pushMessage: PushMessage) async throws {
         guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
             await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
             return
@@ -72,7 +72,7 @@ extension TrioRemoteControl {
             fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
 
-        await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
+        try await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
 
         debug(
             .remoteControl,

+ 37 - 23
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift

@@ -12,21 +12,35 @@ extension TrioRemoteControl {
     }
 
     @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
-        guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
-            await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
-            return
-        }
+        do {
+            guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
+                await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+                return
+            }
 
-        let presetIDs = await overrideStorage.fetchForOverridePresets()
+            let presetIDs = try await overrideStorage.fetchForOverridePresets()
 
-        let presets = presetIDs.compactMap { id in
-            try? viewContext.existingObject(with: id) as? OverrideStored
-        }
+            let presets = try presetIDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OverrideStored
+            }
 
-        if let preset = presets.first(where: { $0.name == overrideName }) {
-            await enactOverridePreset(preset: preset, pushMessage: pushMessage)
-        } else {
-            await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
+            if let preset = presets.first(where: { $0.name == overrideName }) {
+                await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+            } else {
+                await logError(
+                    "Command rejected: override preset '\(overrideName)' not found.",
+                    pushMessage: pushMessage
+                )
+            }
+        } catch {
+            debug(
+                .remoteControl,
+                "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error.localizedDescription)"
+            )
+            await logError(
+                "Command failed: \(error.localizedDescription)",
+                pushMessage: pushMessage
+            )
         }
     }
 
@@ -52,10 +66,10 @@ extension TrioRemoteControl {
     }
 
     @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
+        do {
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
-        let didPostNotification = await viewContext.perform { () -> Bool in
-            do {
+            let didPostNotification = try await viewContext.perform { () -> Bool in
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
@@ -89,16 +103,16 @@ extension TrioRemoteControl {
                 } else {
                     return false
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
-                )
-                return false
             }
-        }
 
-        if didPostNotification {
-            await awaitNotification(.didUpdateOverrideConfiguration)
+            if didPostNotification {
+                await awaitNotification(.didUpdateOverrideConfiguration)
+            }
+        } catch {
+            debug(
+                .remoteControl,
+                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error.localizedDescription)"
+            )
         }
     }
 }

+ 13 - 13
Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -47,10 +47,10 @@ extension TrioRemoteControl {
     }
 
     @MainActor func disableAllActiveTempTargets() async {
-        let ids = await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
+        do {
+            let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
 
-        let didPostNotification = await viewContext.perform { () -> Bool in
-            do {
+            let didPostNotification = try await viewContext.perform { () -> Bool in
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
                 }
@@ -68,8 +68,7 @@ extension TrioRemoteControl {
                     newTempTargetRunStored.name = canceledTempTarget.name
                     newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                     newTempTargetRunStored.endDate = Date()
-                    newTempTargetRunStored
-                        .target = canceledTempTarget.target ?? 0
+                    newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.isUploadedToNS = false
 
@@ -87,16 +86,17 @@ extension TrioRemoteControl {
                 } else {
                     return false
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
-                )
-                return false
             }
-        }
 
-        if didPostNotification {
-            await awaitNotification(.didUpdateTempTargetConfiguration)
+            if didPostNotification {
+                await awaitNotification(.didUpdateTempTargetConfiguration)
+            }
+        } catch {
+            debug(
+                .remoteControl,
+                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error.localizedDescription)"
+            )
+            await logError("Failed to disable temp targets: \(error.localizedDescription)")
         }
     }
 }

+ 4 - 4
Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift

@@ -22,7 +22,7 @@ class TrioRemoteControl: Injectable {
         injectServices(TrioApp.resolver)
     }
 
-    func handleRemoteNotification(pushMessage: PushMessage) async {
+    func handleRemoteNotification(pushMessage: PushMessage) async throws {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
@@ -67,16 +67,16 @@ class TrioRemoteControl: Injectable {
 
         switch pushMessage.commandType {
         case .bolus:
-            await handleBolusCommand(pushMessage)
+            try await handleBolusCommand(pushMessage)
         case .tempTarget:
             await handleTempTargetCommand(pushMessage)
         case .cancelTempTarget:
             await cancelTempTarget(pushMessage)
         case .meal:
-            await handleMealCommand(pushMessage)
+            try await handleMealCommand(pushMessage)
 
             if pushMessage.bolusAmount != nil {
-                await handleBolusCommand(pushMessage)
+                try await handleBolusCommand(pushMessage)
             }
         case .startOverride:
             await handleStartOverrideCommand(pushMessage)

+ 3 - 3
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -241,8 +241,8 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         )
     }
 
-    private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucoseIDs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor20MinAgo,
@@ -261,7 +261,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     @MainActor private func sendGlucoseNotification() async {
         do {
             addAppBadge(glucose: nil)
-            let glucoseIDs = await fetchGlucoseIDs()
+            let glucoseIDs = try await fetchGlucoseIDs()
             let glucoseObjects = try glucoseIDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? GlucoseStored
             }

+ 155 - 136
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -149,159 +149,171 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     /// Prepares the current state data to be sent to the Watch
     /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
     func setupWatchState() async -> WatchState {
-        // Get NSManagedObjectIDs
-        let glucoseIds = await fetchGlucose()
-        // TODO: - if we want that the watch immediately displays updated cob and iob values when entered via treatment view from phone, we would need to use a predicate here that also filters for NON-ENACTED Determinations
-        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
-        let overridePresetIds = await overrideStorage.fetchForOverridePresets()
-        let tempTargetPresetIds = await tempTargetStorage.fetchForTempTargetPresets()
-
-        // Get NSManagedObjects
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: glucoseIds, context: backgroundContext)
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationIds, context: backgroundContext)
-        let overridePresetObjects: [OverrideStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
-        let tempTargetPresetObjects: [TempTargetStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
-
-        return await backgroundContext.perform {
-            var watchState = WatchState(date: Date())
-
-            // Set lastLoopDate
-            let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
-            if lastLoopMinutes > 1440 {
-                watchState.lastLoopTime = "--"
-            } else {
-                watchState.lastLoopTime = "\(lastLoopMinutes) min"
-            }
+        do {
+            // Get NSManagedObjectIDs
+            let glucoseIds = try await fetchGlucose()
+            let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
+            )
+            let overridePresetIds = try await overrideStorage.fetchForOverridePresets()
+            let tempTargetPresetIds = try await tempTargetStorage.fetchForTempTargetPresets()
+
+            // Get NSManagedObjects
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseIds, context: backgroundContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationIds, context: backgroundContext)
+            let overridePresetObjects: [OverrideStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
+            let tempTargetPresetObjects: [TempTargetStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
+
+            return await backgroundContext.perform {
+                var watchState = WatchState(date: Date())
+
+                // Set lastLoopDate
+                let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
+                if lastLoopMinutes > 1440 {
+                    watchState.lastLoopTime = "--"
+                } else {
+                    watchState.lastLoopTime = "\(lastLoopMinutes) min"
+                }
 
-            // Set IOB and COB from latest determination
-            if let latestDetermination = determinationObjects.first {
-                let iob = latestDetermination.iob ?? 0
-                watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
+                // Set IOB and COB from latest determination
+                if let latestDetermination = determinationObjects.first {
+                    let iob = latestDetermination.iob ?? 0
+                    watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
 
-                let cob = NSNumber(value: latestDetermination.cob)
-                watchState.cob = Formatter.integerFormatter.string(from: cob)
-            }
+                    let cob = NSNumber(value: latestDetermination.cob)
+                    watchState.cob = Formatter.integerFormatter.string(from: cob)
+                }
 
-            // Set override presets with their enabled status
-            watchState.overridePresets = overridePresetObjects.map { override in
-                OverridePresetWatch(
-                    name: override.name ?? "",
-                    isEnabled: override.enabled
-                )
-            }
+                // Set override presets with their enabled status
+                watchState.overridePresets = overridePresetObjects.map { override in
+                    OverridePresetWatch(
+                        name: override.name ?? "",
+                        isEnabled: override.enabled
+                    )
+                }
 
-            guard let latestGlucose = glucoseObjects.first else {
-                return watchState
-            }
+                guard let latestGlucose = glucoseObjects.first else {
+                    return watchState
+                }
 
-            // Assign currentGlucose and its color
-            /// Set current glucose with proper formatting
-            if self.units == .mgdL {
-                watchState.currentGlucose = "\(latestGlucose.glucose)"
-            } else {
-                let mgdlValue = Decimal(latestGlucose.glucose)
-                let latestGlucoseValue = mgdlValue.formattedAsMmolL
-                watchState.currentGlucose = "\(latestGlucoseValue)"
-            }
-
-            /// Calculate latest color
-            let hardCodedLow = Decimal(55)
-            let hardCodedHigh = Decimal(220)
-            let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
-
-            let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
-            let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
-            let highGlucoseColorValue = highGlucoseValue
-            let lowGlucoseColorValue = lowGlucoseValue
-            let targetGlucose = self.currentGlucoseTarget
-
-            let currentGlucoseColor = Trio.getDynamicGlucoseColor(
-                glucoseValue: Decimal(latestGlucose.glucose),
-                highGlucoseColorValue: highGlucoseColorValue,
-                lowGlucoseColorValue: lowGlucoseColorValue,
-                targetGlucose: targetGlucose,
-                glucoseColorScheme: self.glucoseColorScheme
-            )
+                // Assign currentGlucose and its color
+                /// Set current glucose with proper formatting
+                if self.units == .mgdL {
+                    watchState.currentGlucose = "\(latestGlucose.glucose)"
+                } else {
+                    let mgdlValue = Decimal(latestGlucose.glucose)
+                    let latestGlucoseValue = mgdlValue.formattedAsMmolL
+                    watchState.currentGlucose = "\(latestGlucoseValue)"
+                }
 
-            if Decimal(latestGlucose.glucose) <= self.lowGlucose || Decimal(latestGlucose.glucose) >= self.highGlucose {
-                watchState.currentGlucoseColorString = currentGlucoseColor.toHexString()
-            } else {
-                watchState.currentGlucoseColorString = "#ffffff" // white when in range; colored when out of range
-            }
+                /// Calculate latest color
+                let hardCodedLow = Decimal(55)
+                let hardCodedHigh = Decimal(220)
+                let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
 
-            // Map glucose values
-            watchState.glucoseValues = glucoseObjects.compactMap { glucose in
-                let glucoseValue = self.units == .mgdL
-                    ? Double(glucose.glucose)
-                    : Double(truncating: Decimal(glucose.glucose).asMmolL as NSNumber)
+                let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
+                let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
+                let highGlucoseColorValue = highGlucoseValue
+                let lowGlucoseColorValue = lowGlucoseValue
+                let targetGlucose = self.currentGlucoseTarget
 
-                let glucoseColor = Trio.getDynamicGlucoseColor(
-                    glucoseValue: Decimal(glucose.glucose),
+                let currentGlucoseColor = Trio.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(latestGlucose.glucose),
                     highGlucoseColorValue: highGlucoseColorValue,
                     lowGlucoseColorValue: lowGlucoseColorValue,
                     targetGlucose: targetGlucose,
                     glucoseColorScheme: self.glucoseColorScheme
                 )
 
-                return WatchGlucoseObject(date: glucose.date ?? Date(), glucose: glucoseValue, color: glucoseColor.toHexString())
-            }
-            .sorted { $0.date < $1.date }
+                if Decimal(latestGlucose.glucose) <= self.lowGlucose || Decimal(latestGlucose.glucose) >= self.highGlucose {
+                    watchState.currentGlucoseColorString = currentGlucoseColor.toHexString()
+                } else {
+                    watchState.currentGlucoseColorString = "#ffffff" // white when in range; colored when out of range
+                }
 
-            // Convert direction to trend string
-            watchState.trend = latestGlucose.direction
+                // Map glucose values
+                watchState.glucoseValues = glucoseObjects.compactMap { glucose in
+                    let glucoseValue = self.units == .mgdL
+                        ? Double(glucose.glucose)
+                        : Double(truncating: Decimal(glucose.glucose).asMmolL as NSNumber)
+
+                    let glucoseColor = Trio.getDynamicGlucoseColor(
+                        glucoseValue: Decimal(glucose.glucose),
+                        highGlucoseColorValue: highGlucoseColorValue,
+                        lowGlucoseColorValue: lowGlucoseColorValue,
+                        targetGlucose: targetGlucose,
+                        glucoseColorScheme: self.glucoseColorScheme
+                    )
+
+                    return WatchGlucoseObject(
+                        date: glucose.date ?? Date(),
+                        glucose: glucoseValue,
+                        color: glucoseColor.toHexString()
+                    )
+                }
+                .sorted { $0.date < $1.date }
+
+                // Convert direction to trend string
+                watchState.trend = latestGlucose.direction
+
+                // Calculate delta if we have at least 2 readings
+                if glucoseObjects.count >= 2 {
+                    var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
 
-            // Calculate delta if we have at least 2 readings
-            if glucoseObjects.count >= 2 {
-                var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
+                    if self.units == .mmolL {
+                        deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    }
 
-                if self.units == .mmolL {
-                    deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    let formattedDelta = Formatter.glucoseFormatter(for: self.units)
+                        .string(from: deltaValue as NSNumber) ?? "0"
+                    watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
                 }
 
-                let formattedDelta = Formatter.glucoseFormatter(for: self.units)
-                    .string(from: deltaValue as NSNumber) ?? "0"
-                watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
-            }
+                // Set temp target presets with their enabled status
+                watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
+                    TempTargetPresetWatch(
+                        name: tempTarget.name ?? "",
+                        isEnabled: tempTarget.enabled
+                    )
+                }
 
-            // Set temp target presets with their enabled status
-            watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
-                TempTargetPresetWatch(
-                    name: tempTarget.name ?? "",
-                    isEnabled: tempTarget.enabled
-                )
-            }
+                // Set units
+                watchState.units = self.units
 
-            // Set units
-            watchState.units = self.units
+                // Add limits and pump specific dosing increment settings values
+                watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
+                watchState.maxCarbs = self.settingsManager.settings.maxCarbs
+                watchState.maxFat = self.settingsManager.settings.maxFat
+                watchState.maxProtein = self.settingsManager.settings.maxProtein
+                watchState.bolusIncrement = self.settingsManager.preferences.bolusIncrement
+                watchState.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
 
-            // Add limits and pump specific dosing increment settings values
-            watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
-            watchState.maxCarbs = self.settingsManager.settings.maxCarbs
-            watchState.maxFat = self.settingsManager.settings.maxFat
-            watchState.maxProtein = self.settingsManager.settings.maxProtein
-            watchState.bolusIncrement = self.settingsManager.preferences.bolusIncrement
-            watchState.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
+                debug(
+                    .watchManager,
+
+                    "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
+                )
 
+                return watchState
+            }
+        } catch {
             debug(
                 .watchManager,
-
-                "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
+                "\(DebuggingIdentifiers.failed) Error setting up watch state: \(error.localizedDescription)"
             )
-
-            return watchState
+            // Return empty state in case of error
+            return WatchState(date: Date())
         }
     }
 
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.glucose,
@@ -319,8 +331,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     /// Fetches last pump event that is a non-external bolus from CoreData
     /// - Returns: NSManagedObjectIDs for last bolus
-    func fetchLastBolus() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.lastPumpBolus,
@@ -338,11 +350,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     /// Gets the active bolus amount by fetching last (active) bolus.
     @MainActor func getActiveBolusAmount() async {
-        if let lastBolusObjectId = await fetchLastBolus() {
-            let lastBolusObject: [PumpEventStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: [lastBolusObjectId], context: viewContext)
+        do {
+            if let lastBolusObjectId = try await fetchLastBolus() {
+                let lastBolusObject: [PumpEventStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: [lastBolusObjectId], context: viewContext)
 
-            activeBolusAmount = lastBolusObject.first?.bolus?.amount?.doubleValue ?? 0.0
+                activeBolusAmount = lastBolusObject.first?.bolus?.amount?.doubleValue ?? 0.0
+            }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error getting active bolus amount: \(error.localizedDescription)"
+            )
         }
     }
 
@@ -678,7 +697,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             let context = CoreDataStack.shared.newTaskContext()
 
-            if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
+            if let overrideId = try await overrideStorage.fetchLatestActiveOverride() {
                 let override = await context.perform {
                     context.object(with: overrideId) as? OverrideStored
                 }
@@ -716,12 +735,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
 
             // Fetch all presets to find the one to activate
-            let presetIds = await overrideStorage.fetchForOverridePresets()
-            let presets: [OverrideStored] = await CoreDataStack.shared
+            let presetIds = try await overrideStorage.fetchForOverridePresets()
+            let presets: [OverrideStored] = try await CoreDataStack.shared
                 .getNSManagedObject(with: presetIds, context: context)
 
             // Check for active override
-            if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
+            if let activeOverrideId = try await overrideStorage.fetchLatestActiveOverride() {
                 let activeOverride = await context.perform {
                     context.object(with: activeOverrideId) as? OverrideStored
                 }
@@ -768,12 +787,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
 
             // Fetch all presets to find the one to activate
-            let presetIds = await tempTargetStorage.fetchForTempTargetPresets()
-            let presets: [TempTargetStored] = await CoreDataStack.shared
+            let presetIds = try await tempTargetStorage.fetchForTempTargetPresets()
+            let presets: [TempTargetStored] = try await CoreDataStack.shared
                 .getNSManagedObject(with: presetIds, context: context)
 
             // Check for active temp target
-            if let activeTempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
+            if let activeTempTargetId = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
                 let activeTempTarget = await context.perform {
                     context.object(with: activeTempTargetId) as? TempTargetStored
                 }
@@ -840,7 +859,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             let context = CoreDataStack.shared.newTaskContext()
 
-            if let tempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
+            if let tempTargetId = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
                 let tempTarget = await context.perform {
                     context.object(with: tempTargetId) as? TempTargetStored
                 }

+ 105 - 90
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -53,7 +53,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Stores, retrieves, and updates insulin dose determinations in CoreData.
     @Injected() private var determinationStorage: DeterminationStorage!
 
-    /// Persists the users device list between app launches.
+    /// Persists the user's device list between app launches.
     @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [GarminDevice] = []
 
     /// Router for presenting alerts or navigation flows (injected via Swinject).
@@ -134,9 +134,16 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 Task {
-                    let watchState = await self.setupGarminWatchState()
-                    let watchStateData = try JSONEncoder().encode(watchState)
-                    self.sendWatchStateData(watchStateData)
+                    do {
+                        let watchState = try await self.setupGarminWatchState()
+                        let watchStateData = try JSONEncoder().encode(watchState)
+                        self.sendWatchStateData(watchStateData)
+                    } catch {
+                        debug(
+                            .watchManager,
+                            "\(DebuggingIdentifiers.failed) Error updating watch state: \(error.localizedDescription)"
+                        )
+                    }
                 }
             }
             .store(in: &subscriptions)
@@ -154,7 +161,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 Task {
-                    let watchState = await self.setupGarminWatchState()
+                    let watchState = try await self.setupGarminWatchState()
                     let watchStateData = try JSONEncoder().encode(watchState)
                     self.sendWatchStateData(watchStateData)
                 }
@@ -167,7 +174,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 Task {
-                    let watchState = await self.setupGarminWatchState()
+                    let watchState = try await self.setupGarminWatchState()
                     let watchStateData = try JSONEncoder().encode(watchState)
                     self.sendWatchStateData(watchStateData)
                 }
@@ -177,8 +184,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
 
     /// Fetches recent glucose readings from CoreData, up to 288 results.
     /// - Returns: An array of `NSManagedObjectID`s for glucose readings.
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.glucose,
@@ -195,101 +202,109 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
 
     /// Builds a `GarminWatchState` reflecting the latest glucose, trend, delta, eventual BG, ISF, IOB, and COB.
     /// - Returns: A `GarminWatchState` containing the most recent device- and therapy-related info.
-    func setupGarminWatchState() async -> GarminWatchState {
-        // Get Glucose IDs
-        let glucoseIds = await fetchGlucose()
-
-        // Fetch the latest OrefDetermination object if available
-        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
-
-        // Turn those IDs into live NSManagedObjects
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: glucoseIds, context: backgroundContext)
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationIds, context: backgroundContext)
-
-        // Perform logic on the background context
-        return await backgroundContext.perform {
-            var watchState = GarminWatchState()
+    func setupGarminWatchState() async throws -> GarminWatchState {
+        do {
+            // Get Glucose IDs
+            let glucoseIds = try await fetchGlucose()
+
+            // Fetch the latest OrefDetermination object if available
+            let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
+            )
 
-            /// Pull `glucose`, `trendRaw`, `delta`, `lastLoopDateInterval`, `iob`, `cob`,  `isf`, and `eventualBGRaw` from the latest determination.
-            if let latestDetermination = determinationObjects.first {
-                watchState.lastLoopDateInterval = latestDetermination.timestamp.map {
-                    guard $0.timeIntervalSince1970 > 0 else { return 0 }
-                    return UInt64($0.timeIntervalSince1970)
+            // Turn those IDs into live NSManagedObjects
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseIds, context: backgroundContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationIds, context: backgroundContext)
+
+            // Perform logic on the background context
+            return await backgroundContext.perform {
+                var watchState = GarminWatchState()
+
+                /// Pull `glucose`, `trendRaw`, `delta`, `lastLoopDateInterval`, `iob`, `cob`,  `isf`, and `eventualBGRaw` from the latest determination.
+                if let latestDetermination = determinationObjects.first {
+                    watchState.lastLoopDateInterval = latestDetermination.timestamp.map {
+                        guard $0.timeIntervalSince1970 > 0 else { return 0 }
+                        return UInt64($0.timeIntervalSince1970)
+                    }
+
+                    let iobValue = latestDetermination.iob ?? 0
+                    watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iobValue)
+
+                    let cobNumber = NSNumber(value: latestDetermination.cob)
+                    watchState.cob = Formatter.integerFormatter.string(from: cobNumber)
+
+                    let insulinSensitivity = latestDetermination.insulinSensitivity ?? 0
+                    let eventualBG = latestDetermination.eventualBG ?? 0
+
+                    if self.units == .mgdL {
+                        watchState.isf = insulinSensitivity.description
+                        watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
+                            .string(from: eventualBG) ?? "0"
+                    } else {
+                        let parsedIsf = Double(truncating: insulinSensitivity).asMmolL
+                        let parsedEventualBG = Double(truncating: eventualBG).asMmolL
+
+                        watchState.isf = parsedIsf.description
+                        watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
+                            .string(from: parsedEventualBG as NSNumber) ?? "0"
+                    }
                 }
 
-                let iobValue = latestDetermination.iob ?? 0
-                watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iobValue)
-
-                let cobNumber = NSNumber(value: latestDetermination.cob)
-                watchState.cob = Formatter.integerFormatter.string(from: cobNumber)
-
-                let insulinSensitivity = latestDetermination.insulinSensitivity ?? 0
-                let eventualBG = latestDetermination.eventualBG ?? 0
+                // If no glucose data is present, just return partial watch state
+                guard let latestGlucose = glucoseObjects.first else {
+                    return watchState
+                }
 
+                // Format the current glucose reading
                 if self.units == .mgdL {
-                    watchState.isf = insulinSensitivity.description
-                    watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
-                        .string(from: eventualBG) ?? "0"
+                    watchState.glucose = "\(latestGlucose.glucose)"
                 } else {
-                    let parsedIsf = Double(truncating: insulinSensitivity).asMmolL
-                    let parsedEventualBG = Double(truncating: eventualBG).asMmolL
-
-                    watchState.isf = parsedIsf.description
-                    watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
-                        .string(from: parsedEventualBG as NSNumber) ?? "0"
+                    let mgdlValue = Decimal(latestGlucose.glucose)
+                    let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
+                    watchState.glucose = "\(latestGlucoseValue)"
                 }
-            }
-
-            // If no glucose data is present, just return partial watch state
-            guard let latestGlucose = glucoseObjects.first else {
-                return watchState
-            }
 
-            // Format the current glucose reading
-            if self.units == .mgdL {
-                watchState.glucose = "\(latestGlucose.glucose)"
-            } else {
-                let mgdlValue = Decimal(latestGlucose.glucose)
-                let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
-                watchState.glucose = "\(latestGlucoseValue)"
-            }
+                // Convert direction to a textual trend
+                watchState.trendRaw = latestGlucose.direction ?? "--"
 
-            // Convert direction to a textual trend
-            watchState.trendRaw = latestGlucose.direction ?? "--"
+                // Calculate a glucose delta if we have at least two readings
+                if glucoseObjects.count >= 2 {
+                    var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
 
-            // Calculate a glucose delta if we have at least two readings
-            if glucoseObjects.count >= 2 {
-                var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
+                    if self.units == .mmolL {
+                        deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    }
 
-                if self.units == .mmolL {
-                    deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    let formattedDelta = Formatter.glucoseFormatter(for: self.units)
+                        .string(from: deltaValue as NSNumber) ?? "0"
+                    watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
                 }
 
-                let formattedDelta = Formatter.glucoseFormatter(for: self.units)
-                    .string(from: deltaValue as NSNumber) ?? "0"
-                watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
-            }
+                debug(
+                    .watchManager,
+                    """
+                    📱 Setup GarminWatchState - \
+                    glucose: \(watchState.glucose ?? "nil"), \
+                    trendRaw: \(watchState.trendRaw ?? "nil"), \
+                    delta: \(watchState.delta ?? "nil"), \
+                    eventualBGRaw: \(watchState.eventualBGRaw ?? "nil"), \
+                    isf: \(watchState.isf ?? "nil"), \
+                    cob: \(watchState.cob ?? "nil"), \
+                    iob: \(watchState.iob ?? "nil"), \
+                    lastLoopDateInterval: \(watchState.lastLoopDateInterval?.description ?? "nil")
+                    """
+                )
 
+                return watchState
+            }
+        } catch {
             debug(
                 .watchManager,
-                """
-                📱 Setup GarminWatchState - \
-                glucose: \(watchState.glucose ?? "nil"), \
-                trendRaw: \(watchState.trendRaw ?? "nil"), \
-                delta: \(watchState.delta ?? "nil"), \
-                eventualBGRaw: \(watchState.eventualBGRaw ?? "nil"), \
-                isf: \(watchState.isf ?? "nil"), \
-                cob: \(watchState.cob ?? "nil"), \
-                iob: \(watchState.iob ?? "nil"), \
-                lastLoopDateInterval: \(watchState.lastLoopDateInterval?.description ?? "nil")
-                """
+                "\(DebuggingIdentifiers.failed) Error setting up Garmin watch state: \(error.localizedDescription)"
             )
-
-            return watchState
+            throw error
         }
     }
 
@@ -357,7 +372,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     }
 
     /// Subscribes to any watch-state dictionaries published via `watchStateSubject`, and throttles them
-    /// so updates arent sent too frequently. Each update triggers a broadcast to all watch apps.
+    /// so updates aren't sent too frequently. Each update triggers a broadcast to all watch apps.
     private func subscribeToWatchState() {
         watchStateSubject
             .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
@@ -419,7 +434,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
         .eraseToAnyPublisher()
     }
 
-    /// Updates the managers list of devices, typically after user selection or manual changes.
+    /// Updates the manager's list of devices, typically after user selection or manual changes.
     /// - Parameter devices: The new array of `IQDevice` objects to track.
     func updateDeviceList(_ devices: [IQDevice]) {
         self.devices = devices
@@ -527,7 +542,7 @@ extension BaseGarminManager: IQUIOverrideDelegate, IQDeviceEventDelegate, IQAppM
 
             do {
                 // Fetch the latest watch state (async) and encode it to JSON data
-                let watchState = await self.setupGarminWatchState()
+                let watchState = try await self.setupGarminWatchState()
                 let watchStateData = try JSONEncoder().encode(watchState)
 
                 // Now send that JSON data to the watch
@@ -560,7 +575,7 @@ extension BaseGarminManager: SettingsObserver {
         units = settingsManager.settings.units
 
         Task {
-            let watchState = await setupGarminWatchState()
+            let watchState = try await setupGarminWatchState()
             let watchStateData = try JSONEncoder().encode(watchState)
             sendWatchStateData(watchStateData)
         }

+ 1 - 1
Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -15,7 +15,7 @@ import Foundation
 
         let carbs = min(Decimal(quantityCarbs), settingsManager.settings.maxCarbs)
 
-        await carbsStorage.storeCarbs(
+        try await carbsStorage.storeCarbs(
             [CarbsEntry(
                 id: UUID().uuidString,
                 createdAt: dateAdded,

+ 2 - 2
Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift

@@ -18,10 +18,10 @@ struct OverridePreset: AppEntity, Identifiable {
 
 struct OverridePresetsQuery: EntityQuery {
     func entities(for identifiers: [OverridePreset.ID]) async throws -> [OverridePreset] {
-        await OverridePresetsIntentRequest().fetchIDs(identifiers)
+        try await OverridePresetsIntentRequest().fetchIDs(identifiers)
     }
 
     func suggestedEntities() async throws -> [OverridePreset] {
-        await OverridePresetsIntentRequest().fetchAndProcessOverrides()
+        try await OverridePresetsIntentRequest().fetchAndProcessOverrides()
     }
 }

+ 45 - 48
Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift

@@ -9,13 +9,13 @@ import UIKit
         case noActiveOverride
     }
 
-    func fetchAndProcessOverrides() async -> [OverridePreset] {
-        // Fetch all Override Presets via OverrideStorage
-        let allOverridePresetsIDs = await overrideStorage.fetchForOverridePresets()
+    func fetchAndProcessOverrides() async throws -> [OverridePreset] {
+        do {
+            // Fetch all Override Presets via OverrideStorage
+            let allOverridePresetsIDs = try await overrideStorage.fetchForOverridePresets()
 
-        // Since we are fetching on a different background Thread we need to unpack the NSManagedObjectID on the correct Thread first
-        return await coredataContext.perform {
-            do {
+            // Since we are fetching on a different background Thread we need to unpack the NSManagedObjectID on the correct Thread first
+            return try await coredataContext.perform {
                 let overrideObjects = try allOverridePresetsIDs.compactMap { id in
                     try self.coredataContext.existingObject(with: id) as? OverrideStored
                 }
@@ -25,18 +25,18 @@ import UIKit
                           let name = object.name else { return OverridePreset(id: UUID().uuidString, name: "") }
                     return OverridePreset(id: id, name: name)
                 }
-
-            } catch {
-                debugPrint(
-                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while fetching/ processing the overrides Array: \(error.localizedDescription)"
-                )
-                return [OverridePreset(id: UUID().uuidString, name: "")]
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error fetching/processing overrides: \(error.localizedDescription)"
+            )
+            throw error
         }
     }
 
-    func fetchIDs(_ uuid: [OverridePreset.ID]) async -> [OverridePreset] {
-        await coredataContext.perform {
+    func fetchIDs(_ uuid: [OverridePreset.ID]) async throws -> [OverridePreset] {
+        try await coredataContext.perform {
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", uuid)
 
@@ -44,36 +44,40 @@ import UIKit
                 let result = try self.coredataContext.fetch(fetchRequest)
 
                 if result.isEmpty {
-                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) No OverrideStored found for ids: \(uuid)")
-                    return [OverridePreset(id: UUID().uuidString, name: "")]
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) No OverrideStored found for ids: \(uuid)"
+                    )
+                    throw overridePresetsError.noTempOverrideFound
                 }
 
                 return result.map { overrideStored in
                     OverridePreset(id: overrideStored.id ?? UUID().uuidString, name: overrideStored.name ?? "")
                 }
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Override: \(error.localizedDescription)"
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to fetch Override: \(error.localizedDescription)"
                 )
-                return [OverridePreset(id: UUID().uuidString, name: "")]
+                throw error
             }
         }
     }
 
-    private func fetchOverrideID(_ preset: OverridePreset) async -> NSManagedObjectID? {
+    private func fetchOverrideID(_ preset: OverridePreset) async throws -> NSManagedObjectID {
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
         fetchRequest.fetchLimit = 1
 
-        return await coredataContext.perform {
-            do {
-                return try self.coredataContext.fetch(fetchRequest).first?.objectID
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Override: \(error.localizedDescription)"
+        return try await coredataContext.perform {
+            guard let objectID = try self.coredataContext.fetch(fetchRequest).first?.objectID else {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) No override found for preset: \(preset.name)"
                 )
-                return nil
+                throw overridePresetsError.noTempOverrideFound
             }
+            return objectID
         }
     }
 
@@ -83,13 +87,11 @@ import UIKit
         backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Override Upload") {
             guard backgroundTaskID != .invalid else { return }
             Task {
-                // End background task when the time is about to expire
                 UIApplication.shared.endBackgroundTask(backgroundTaskID)
             }
             backgroundTaskID = .invalid
         }
 
-        // Defer block to end background task when function exits
         defer {
             if backgroundTaskID != .invalid {
                 Task {
@@ -101,37 +103,33 @@ import UIKit
 
         do {
             // Get NSManagedObjectID of Preset
-            guard let overrideID = await fetchOverrideID(preset),
-                  let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored
-            else { return false }
+            let overrideID = try await fetchOverrideID(preset)
+            guard let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored else {
+                throw overridePresetsError.noTempOverrideFound
+            }
 
             // Enable Override
             overrideObject.enabled = true
             overrideObject.date = Date()
             overrideObject.isUploadedToNS = false
 
-            // Disable previous overrides if necessary, without starting a background task
+            // Disable previous overrides if necessary
             await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true, shouldStartBackgroundTask: false)
 
             if viewContext.hasChanges {
                 try viewContext.save()
-
-                // Update State variables in OverrideView
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
-
-                // Await the notification
-                print("Waiting for notification...")
                 await awaitNotification(.didUpdateOverrideConfiguration)
-                print("Notification received, continuing...")
-
                 return true
             }
+            return false
         } catch {
-            // Handle error and ensure background task is ended
-            debugPrint("Failed to enact Override: \(error.localizedDescription)")
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to enact override: \(error.localizedDescription)"
+            )
+            return false
         }
-
-        return false
     }
 
     func cancelOverride() async {
@@ -167,10 +165,9 @@ import UIKit
             }
         }
 
-        // Get NSManagedObjectID of all active overrides
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
         do {
+            // Get NSManagedObjectID of all active overrides
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
             // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? OverrideStored

+ 2 - 2
Trio/Sources/Shortcuts/State/StateIntentRequest.swift

@@ -61,7 +61,7 @@ final class StateIntentRequest: BaseIntentsRequest {
         -> (dateGlucose: Date, glucose: String, trend: String, delta: String)
     {
         do {
-            let results = CoreDataStack.shared.fetchEntities(
+            let results = try CoreDataStack.shared.fetchEntities(
                 ofType: GlucoseStored.self,
                 onContext: onContext,
                 predicate: NSPredicate.predicateFor30MinAgo,
@@ -102,7 +102,7 @@ final class StateIntentRequest: BaseIntentsRequest {
     }
 
     func getIobAndCob(onContext: NSManagedObjectContext) throws -> (iob: Double, cob: Double) {
-        let results = CoreDataStack.shared.fetchEntities(
+        let results = try CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             onContext: onContext,
             predicate: NSPredicate.enactedDetermination,

+ 1 - 1
Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift

@@ -25,6 +25,6 @@ struct TempPresetsQuery: EntityQuery {
     }
 
     func suggestedEntities() async throws -> [TempPreset] {
-        await TempPresetsIntentRequest().fetchAndProcessTempTargets()
+        try await TempPresetsIntentRequest().fetchAndProcessTempTargets()
     }
 }

+ 4 - 5
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -8,9 +8,9 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
         case noDurationDefined
     }
 
-    func fetchAndProcessTempTargets() async -> [TempPreset] {
+    func fetchAndProcessTempTargets() async throws -> [TempPreset] {
         // Fetch all Temp Target Presets via TempTargetStorage
-        let allTempTargetPresetsIDs = await tempTargetsStorage.fetchForTempTargetPresets()
+        let allTempTargetPresetsIDs = try await tempTargetsStorage.fetchForTempTargetPresets()
 
         // Perform the fetch and process on the Core Data context's thread
         return await coredataContext.perform {
@@ -198,10 +198,9 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
             }
         }
 
-        // Get NSManagedObjectID of all active temp Targets
-        let ids = await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-
         do {
+            // Get NSManagedObjectID of all active temp Targets
+            let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
             // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? TempTargetStored