فهرست منبع

Merge pull request #334 from nightscout/dev

[DRAFT] v0.3.0 (10)
Mike Plante 1 سال پیش
والد
کامیت
bc6a51dd38
100فایلهای تغییر یافته به همراه184007 افزوده شده و 52606 حذف شده
  1. 1 3
      .gitignore
  2. 1 1
      G7SensorKit
  3. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  4. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift
  5. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift
  6. 90 16
      Model/CoreDataObserver.swift
  7. 129 80
      Model/CoreDataStack.swift
  8. 24 21
      Model/Helper/CoreDataError.swift
  9. 21 0
      Model/Helper/GlucoseStored+helper.swift
  10. 31 1
      Model/Helper/PumpEvent+helper.swift
  11. 11 0
      Model/Helper/TempTargetStored+Helper.swift
  12. 16 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  13. 4 0
      TDDStored+CoreDataClass.swift
  14. 18 0
      TDDStored+CoreDataProperties.swift
  15. 11 0
      Trio Watch App Extension/Views/AcknowledgementPendingView.swift
  16. 1 1
      Trio Watch App Extension/Views/BolusConfirmationView.swift
  17. 9 3
      Trio Watch App Extension/Views/BolusInputView.swift
  18. 4 1
      Trio Watch App Extension/Views/BolusProgressOverlay.swift
  19. 3 2
      Trio Watch App Extension/Views/CarbsInputView.swift
  20. 8 3
      Trio Watch App Extension/Views/GlucoseTrendView.swift
  21. 3 3
      Trio Watch App Extension/Views/TreatmentMenuView.swift
  22. 3 5
      Trio Watch App Extension/WatchState.swift
  23. 64 151
      Trio.xcodeproj/project.pbxproj
  24. 1 1
      Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch App.xcscheme
  25. 1 1
      Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch Complication Extension.xcscheme
  26. 2 2
      Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  27. 0 23
      Trio/Resources/Base.lproj/InfoPlist.strings
  28. 1059 0
      Trio/Resources/InfoPlist.xcstrings
  29. 0 20
      Trio/Resources/ar.lproj/InfoPlist.strings
  30. 0 20
      Trio/Resources/ca.lproj/InfoPlist.strings
  31. 0 20
      Trio/Resources/da.lproj/InfoPlist.strings
  32. 0 20
      Trio/Resources/de.lproj/InfoPlist.strings
  33. 0 20
      Trio/Resources/es.lproj/InfoPlist.strings
  34. 0 20
      Trio/Resources/fi.lproj/InfoPlist.strings
  35. 0 20
      Trio/Resources/fr.lproj/InfoPlist.strings
  36. 0 20
      Trio/Resources/he.lproj/InfoPlist.strings
  37. 0 20
      Trio/Resources/hu.lproj/InfoPlist.strings
  38. 0 20
      Trio/Resources/it.lproj/InfoPlist.strings
  39. 0 20
      Trio/Resources/nb.lproj/InfoPlist.strings
  40. 0 20
      Trio/Resources/nl.lproj/InfoPlist.strings
  41. 0 20
      Trio/Resources/pl.lproj/InfoPlist.strings
  42. 0 20
      Trio/Resources/pt-BR.lproj/InfoPlist.strings
  43. 0 20
      Trio/Resources/pt-PT.lproj/InfoPlist.strings
  44. 0 20
      Trio/Resources/ru.lproj/InfoPlist.strings
  45. 0 20
      Trio/Resources/sk.lproj/InfoPlist.strings
  46. 0 20
      Trio/Resources/sv.lproj/InfoPlist.strings
  47. 0 20
      Trio/Resources/tr.lproj/InfoPlist.strings
  48. 0 20
      Trio/Resources/uk.lproj/InfoPlist.strings
  49. 0 20
      Trio/Resources/vi.lproj/InfoPlist.strings
  50. 0 20
      Trio/Resources/zh-Hans.lproj/InfoPlist.strings
  51. 352 321
      Trio/Sources/APS/APSManager.swift
  52. 1 1
      Trio/Sources/APS/CGM/AppGroupSource.swift
  53. 8 7
      Trio/Sources/APS/CGM/CGMType.swift
  54. 185 90
      Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift
  55. 2 2
      Trio/Sources/APS/CGM/PluginSource.swift
  56. 15 9
      Trio/Sources/APS/DeviceDataManager.swift
  57. 59 85
      Trio/Sources/APS/FetchGlucoseManager.swift
  58. 39 35
      Trio/Sources/APS/FetchTreatmentsManager.swift
  59. 41 35
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  60. 31 29
      Trio/Sources/APS/Storage/CarbsStorage.swift
  61. 34 27
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  62. 49 15
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  63. 227 159
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  64. 57 49
      Trio/Sources/APS/Storage/OverrideStorage.swift
  65. 221 202
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  66. 661 0
      Trio/Sources/APS/Storage/TDDStorage.swift
  67. 49 38
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  68. 17 3
      Trio/Sources/Application/AppDelegate.swift
  69. 20 11
      Trio/Sources/Application/TrioApp.swift
  70. 1 0
      Trio/Sources/Assemblies/StorageAssembly.swift
  71. 20 11
      Trio/Sources/Helpers/BuildDetails.swift
  72. 4 4
      Trio/Sources/Helpers/HKUnit.swift
  73. 1 1
      Trio/Sources/Helpers/MainChartHelper.swift
  74. 180375 0
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  75. 0 2223
      Trio/Sources/Localizations/Main/ar.lproj/Localizable.strings
  76. 0 1917
      Trio/Sources/Localizations/Main/ca.lproj/Localizable.strings
  77. 0 2184
      Trio/Sources/Localizations/Main/da.lproj/Localizable.strings
  78. 0 2229
      Trio/Sources/Localizations/Main/de.lproj/Localizable.strings
  79. 0 2231
      Trio/Sources/Localizations/Main/en.lproj/Localizable.strings
  80. 0 2228
      Trio/Sources/Localizations/Main/es.lproj/Localizable.strings
  81. 0 2229
      Trio/Sources/Localizations/Main/fi.lproj/Localizable.strings
  82. 0 2220
      Trio/Sources/Localizations/Main/fr.lproj/Localizable.strings
  83. 0 2223
      Trio/Sources/Localizations/Main/he.lproj/Localizable.strings
  84. 0 2178
      Trio/Sources/Localizations/Main/hu.lproj/Localizable.strings
  85. 0 2225
      Trio/Sources/Localizations/Main/it.lproj/Localizable.strings
  86. 0 2223
      Trio/Sources/Localizations/Main/nb.lproj/Localizable.strings
  87. 0 2231
      Trio/Sources/Localizations/Main/nl.lproj/Localizable.strings
  88. 0 2225
      Trio/Sources/Localizations/Main/pl.lproj/Localizable.strings
  89. 0 2223
      Trio/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  90. 0 2223
      Trio/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  91. 0 2226
      Trio/Sources/Localizations/Main/ru.lproj/Localizable.strings
  92. 0 2178
      Trio/Sources/Localizations/Main/sk.lproj/Localizable.strings
  93. 0 2223
      Trio/Sources/Localizations/Main/sv.lproj/Localizable.strings
  94. 0 2227
      Trio/Sources/Localizations/Main/tr.lproj/Localizable.strings
  95. 0 2223
      Trio/Sources/Localizations/Main/uk.lproj/Localizable.strings
  96. 0 2180
      Trio/Sources/Localizations/Main/vi.lproj/Localizable.strings
  97. 0 2225
      Trio/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  98. 4 0
      Trio/Sources/Logger/Logger.swift
  99. 16 16
      Trio/Sources/Models/ContactTrickEntry.swift
  100. 0 0
      Trio/Sources/Models/ForecastDisplayType.swift

+ 1 - 3
.gitignore

@@ -79,6 +79,4 @@ fastlane/screenshots
 fastlane/test_output
 fastlane/test_output
 fastlane/FastlaneRunner
 fastlane/FastlaneRunner
 
 
-ConfigOverride.xcconfig
-
-branch.txt
+ConfigOverride.xcconfig

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 2be3eb29b0a18aa89f8b60281341e46e07d024e5
+Subproject commit 205054e7537723c2aec58d807634b4853f687244

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift

@@ -22,7 +22,7 @@ struct LiveActivityCOBLabelView: View {
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
 
-                Text("g")
+                Text(String(localized: "g", comment: "gram of carbs"))
                     .font(.headline).fontWeight(.bold)
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift

@@ -30,7 +30,7 @@ struct LiveActivityIOBLabelView: View {
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
 
-                Text("U")
+                Text(String(localized: "U", comment: "Insulin unit"))
                     .font(.headline).fontWeight(.bold)
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift

@@ -24,7 +24,7 @@ struct LiveActivityTotalDailyDoseView: View {
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
 
-                Text("U")
+                Text(String(localized: "U", comment: "Insulin unit"))
                     .font(.headline).fontWeight(.bold)
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))

+ 90 - 16
Model/CoreDataObserver.swift

@@ -2,34 +2,108 @@ import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 
 
-func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObjectID>, Never> {
+/// Represents the types of Core Data changes that can be observed
+/// Use as an option set to specify which types of changes to monitor
+public struct CoreDataChangeTypes: OptionSet {
+    /// The raw integer value used to store the option set bits
+    public let rawValue: Int
+
+    /// Required initializer for OptionSet conformance
+    public init(rawValue: Int) {
+        self.rawValue = rawValue
+    }
+
+    /// Represents newly created/inserted objects in Core Data
+    /// Binary: 001 (1 << 0)
+    public static let inserted = CoreDataChangeTypes(rawValue: 1 << 0)
+
+    /// Represents modified/updated objects in Core Data
+    /// Binary: 010 (1 << 1)
+    public static let updated = CoreDataChangeTypes(rawValue: 1 << 1)
+
+    /// Represents removed/deleted objects in Core Data
+    /// Binary: 100 (1 << 2)
+    public static let deleted = CoreDataChangeTypes(rawValue: 1 << 2)
+
+    /// Convenience option that includes all possible change types
+    /// This combines inserted, updated, and deleted into a single option
+    public static let all: CoreDataChangeTypes = [.inserted, .updated, .deleted]
+}
+
+/// Creates a publisher that emits sets of NSManagedObjectIDs when Core Data changes occur
+/// - Parameter changeTypes: The types of changes to observe (defaults to .all)
+/// - Returns: A publisher that emits Sets of NSManagedObjectIDs for the specified change types
+func changedObjectsOnManagedObjectContextDidSavePublisher(
+    observing changeTypes: CoreDataChangeTypes = .all
+) -> some Publisher<Set<NSManagedObjectID>, Never> {
     Foundation.NotificationCenter.default
     Foundation.NotificationCenter.default
-        .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
-        .map { notification in
-            guard let userInfo = notification.userInfo else { return Set<NSManagedObjectID>() }
+        .publisher(for: .NSManagedObjectContextDidSave)
+        .compactMap { notification -> Set<NSManagedObjectID>? in
 
 
             var objectIDs = Set<NSManagedObjectID>()
             var objectIDs = Set<NSManagedObjectID>()
 
 
-            if let inserted = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> {
-                objectIDs.formUnion(inserted.map(\.objectID))
+            // Process inserted objects if requested
+            if changeTypes.contains(.inserted) {
+                objectIDs.formUnion(notification.insertedObjectIDs)
             }
             }
-            if let updated = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
-                objectIDs.formUnion(updated.map(\.objectID))
+
+            // Process updated objects if requested
+            if changeTypes.contains(.updated) {
+                objectIDs.formUnion(notification.updatedObjectIDs)
             }
             }
-            if let deleted = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> {
-                objectIDs.formUnion(deleted.map(\.objectID))
+
+            // Process deleted objects if requested
+            if changeTypes.contains(.deleted) {
+                objectIDs.formUnion(notification.deletedObjectIDs)
             }
             }
 
 
-            return objectIDs
+            // Only emit non-empty sets
+            return objectIDs.isEmpty ? nil : objectIDs
         }
         }
 }
 }
 
 
 extension Publisher where Output == Set<NSManagedObjectID> {
 extension Publisher where Output == Set<NSManagedObjectID> {
-    func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
-        filter { objectIDs in
-            objectIDs.contains { objectID in
-                objectID.entity.name == name
-            }
+    /// Filters Core Data changes by entity name
+    ///
+    /// This method allows filtering Core Data changes by entity name.
+    ///
+    /// Example usage:
+    /// ```swift
+    /// // Filter changes for "GlucoseStored" entity
+    /// publisher.filteredByEntityName("GlucoseStored")
+    /// ```
+    ///
+    /// - Parameters:
+    ///   - name: The name of the Core Data entity to filter for
+    /// - Returns: A publisher emitting filtered sets of NSManagedObjectIDs
+    func filteredByEntityName(
+        _ name: String
+    ) -> some Publisher<Set<NSManagedObjectID>, Self.Failure> {
+        compactMap { objectIDs -> Set<NSManagedObjectID>? in
+            // Early exit for empty sets
+            guard !objectIDs.isEmpty else { return nil }
+
+            // Use lazy evaluation for better performance
+            let filtered = objectIDs.lazy.filter { $0.entity.name == name }
+            let result = Set(filtered)
+            return result.isEmpty ? nil : result
         }
         }
     }
     }
 }
 }
+
+extension Notification {
+    var insertedObjectIDs: Set<NSManagedObjectID> {
+        guard let objects = userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> else { return [] }
+        return Set(objects.lazy.map(\.objectID))
+    }
+
+    var updatedObjectIDs: Set<NSManagedObjectID> {
+        guard let objects = userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> else { return [] }
+        return Set(objects.lazy.map(\.objectID))
+    }
+
+    var deletedObjectIDs: Set<NSManagedObjectID> {
+        guard let objects = userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> else { return [] }
+        return Set(objects.lazy.map(\.objectID))
+    }
+}

+ 129 - 80
Model/CoreDataStack.swift

@@ -9,9 +9,51 @@ class CoreDataStack: ObservableObject {
     private var notificationToken: NSObjectProtocol?
     private var notificationToken: NSObjectProtocol?
     private let inMemory: Bool
     private let inMemory: Bool
 
 
+    let persistentContainer: NSPersistentContainer
+
     private init(inMemory: Bool = false) {
     private init(inMemory: Bool = false) {
         self.inMemory = inMemory
         self.inMemory = inMemory
 
 
+        // Initialize persistent container immediately
+        persistentContainer = NSPersistentContainer(
+            name: "TrioCoreDataPersistentContainer",
+            managedObjectModel: Self.managedObjectModel
+        )
+
+        guard let description = persistentContainer.persistentStoreDescriptions.first else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to retrieve a persistent store description")
+        }
+
+        if inMemory {
+            description.url = URL(fileURLWithPath: "/dev/null")
+        }
+
+        // Enable persistent store remote change notifications
+        /// - Tag: persistentStoreRemoteChange
+        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
+
+        // Enable persistent history tracking
+        /// - Tag: persistentHistoryTracking
+        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
+
+        // Enable lightweight migration
+        /// - Tag: lightweightMigration
+        description.shouldMigrateStoreAutomatically = true
+        description.shouldInferMappingModelAutomatically = true
+
+        persistentContainer.loadPersistentStores { _, error in
+            if let error = error as NSError? {
+                fatalError("Unresolved Error \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
+            }
+        }
+
+        persistentContainer.viewContext.automaticallyMergesChangesFromParent = false
+        persistentContainer.viewContext.name = "viewContext"
+        /// - Tag: viewContextmergePolicy
+        persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
+        persistentContainer.viewContext.undoManager = nil
+        persistentContainer.viewContext.shouldDeleteInaccessibleFaults = true
+
         // Observe Core Data remote change notifications on the queue where the changes were made
         // Observe Core Data remote change notifications on the queue where the changes were made
         notificationToken = Foundation.NotificationCenter.default.addObserver(
         notificationToken = Foundation.NotificationCenter.default.addObserver(
             forName: .NSPersistentStoreRemoteChange,
             forName: .NSPersistentStoreRemoteChange,
@@ -41,44 +83,33 @@ class CoreDataStack: ObservableObject {
         }
         }
     }
     }
 
 
-    /// A persistent container to set up the Core Data Stack
-    lazy var persistentContainer: NSPersistentContainer = {
-        let container = NSPersistentContainer(name: "TrioCoreDataPersistentContainer")
-
-        guard let description = container.persistentStoreDescriptions.first else {
-            fatalError("Failed \(DebuggingIdentifiers.failed) to retrieve a persistent store description")
-        }
+    // Factory method for tests
+    static func createForTests() -> CoreDataStack {
+        CoreDataStack(inMemory: true)
+    }
 
 
-        if inMemory {
-            description.url = URL(fileURLWithPath: "/dev/null")
-        }
+    // Used for Canvas Preview
+    static var preview: CoreDataStack = {
+        let stack = CoreDataStack(inMemory: true)
+        let context = stack.persistentContainer.viewContext
 
 
-        // Enable persistent store remote change notifications
-        /// - Tag: persistentStoreRemoteChange
-        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
+        let pumpHistory = PumpEventStored.makePreviewEvents(count: 10, provider: stack)
 
 
-        // Enable persistent history tracking
-        /// - Tag: persistentHistoryTracking
-        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
+        return stack
+    }()
 
 
-        // Enable lightweight migration
-        /// - Tag: lightweightMigration
-        description.shouldMigrateStoreAutomatically = true
-        description.shouldInferMappingModelAutomatically = true
+    // Shared managed object model
+    static var managedObjectModel: NSManagedObjectModel = {
+        let bundle = Bundle(for: CoreDataStack.self)
+        guard let url = bundle.url(forResource: "TrioCoreDataPersistentContainer", withExtension: "momd") else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to locate momd file")
+        }
 
 
-        container.loadPersistentStores { _, error in
-            if let error = error as NSError? {
-                fatalError("Unresolved Error \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
-            }
+        guard let model = NSManagedObjectModel(contentsOf: url) else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to load momd file")
         }
         }
 
 
-        container.viewContext.automaticallyMergesChangesFromParent = false
-        container.viewContext.name = "viewContext"
-        /// - Tag: viewContextmergePolicy
-        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
-        container.viewContext.undoManager = nil
-        container.viewContext.shouldDeleteInaccessibleFaults = true
-        return container
+        return model
     }()
     }()
 
 
     /// Creates and configures a private queue context
     /// Creates and configures a private queue context
@@ -88,7 +119,7 @@ class CoreDataStack: ObservableObject {
         let taskContext = persistentContainer.newBackgroundContext()
         let taskContext = persistentContainer.newBackgroundContext()
 
 
         /// ensure that the background contexts stay in sync with the main context
         /// ensure that the background contexts stay in sync with the main context
-        taskContext.automaticallyMergesChangesFromParent = true
+        taskContext.automaticallyMergesChangesFromParent = false
         taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         taskContext.undoManager = nil
         taskContext.undoManager = nil
         return taskContext
         return taskContext
@@ -98,14 +129,14 @@ class CoreDataStack: ObservableObject {
         do {
         do {
             try await fetchPersistentHistoryTransactionsAndChanges()
             try await fetchPersistentHistoryTransactionsAndChanges()
         } catch {
         } catch {
-            debugPrint("\(error.localizedDescription)")
+            debug(.coreData, "\(error.localizedDescription)")
         }
         }
     }
     }
 
 
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "persistentHistoryContext"
         taskContext.name = "persistentHistoryContext"
-        //        debugPrint("Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
+//        debug(.coreData,"Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
 
 
         try await taskContext.perform {
         try await taskContext.perform {
             // Execute the persistent history change since the last transaction
             // Execute the persistent history change since the last transaction
@@ -120,7 +151,7 @@ class CoreDataStack: ObservableObject {
     }
     }
 
 
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
-        //        debugPrint("Received \(history.count) persistent history transactions")
+//        debug(.coreData,"Received \(history.count) persistent history transactions")
         // Update view context with objectIDs from history change request
         // Update view context with objectIDs from history change request
         /// - Tag: mergeChanges
         /// - Tag: mergeChanges
         let viewContext = persistentContainer.viewContext
         let viewContext = persistentContainer.viewContext
@@ -142,10 +173,11 @@ class CoreDataStack: ObservableObject {
             let deleteHistoryTokensRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
             let deleteHistoryTokensRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
             do {
             do {
                 try taskContext.execute(deleteHistoryTokensRequest)
                 try taskContext.execute(deleteHistoryTokensRequest)
-                debugPrint("\(DebuggingIdentifiers.succeeded) Successfully deleted persistent history before \(date)")
+                debug(.coreData, "\(DebuggingIdentifiers.succeeded) Successfully deleted persistent history from before \(date)")
             } catch {
             } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history before \(date): \(error.localizedDescription)"
+                debug(
+                    .coreData,
+                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history from before \(date): \(error.localizedDescription)"
                 )
                 )
             }
             }
         }
         }
@@ -155,9 +187,9 @@ class CoreDataStack: ObservableObject {
         // Force initialization of persistent container
         // Force initialization of persistent container
         let container = persistentContainer
         let container = persistentContainer
 
 
-        // Verify the store is loaded and available
-        guard container.persistentStoreCoordinator.persistentStores.isNotEmpty else {
-            throw CoreDataError.storeNotInitializedError
+        // Verify the store is loaded
+        guard container.persistentStoreCoordinator.persistentStores.isEmpty == false else {
+            throw CoreDataError.storeNotInitializedError(function: #function, file: #file)
         }
         }
     }
     }
 }
 }
@@ -169,7 +201,7 @@ extension CoreDataStack {
     ///  - Tag: synchronousDelete
     ///  - Tag: synchronousDelete
     func deleteObject(identifiedBy objectID: NSManagedObjectID) async {
     func deleteObject(identifiedBy objectID: NSManagedObjectID) async {
         let viewContext = persistentContainer.viewContext
         let viewContext = persistentContainer.viewContext
-        debugPrint("Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
+        debug(.coreData, "Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
 
 
         await viewContext.perform {
         await viewContext.perform {
             do {
             do {
@@ -178,9 +210,9 @@ extension CoreDataStack {
 
 
                 guard viewContext.hasChanges else { return }
                 guard viewContext.hasChanges else { return }
                 try viewContext.save()
                 try viewContext.save()
-                debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
+                debug(.coreData, "Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
             } catch {
             } catch {
-                debugPrint("Failed to delete data: \(error.localizedDescription)")
+                debug(.coreData, "Failed to delete data: \(error.localizedDescription)")
             }
             }
         }
         }
     }
     }
@@ -191,7 +223,9 @@ extension CoreDataStack {
         _ objectType: T.Type,
         _ objectType: T.Type,
         dateKey: String,
         dateKey: String,
         days: Int,
         days: Int,
-        isPresetKey: String? = nil
+        isPresetKey: String? = nil,
+        callingFunction: String = #function,
+        callingClass: String = #fileID
     ) async throws {
     ) async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.name = "deleteContext"
@@ -219,7 +253,7 @@ extension CoreDataStack {
 
 
             // Guard check if there are NSManagedObjects older than the specified days
             // Guard check if there are NSManagedObjects older than the specified days
             guard !objectIDs.isEmpty else {
             guard !objectIDs.isEmpty else {
-//                debugPrint("No objects found older than \(days) days.")
+//                debug(.coreData,"No objects found older than \(days) days.")
                 return
                 return
             }
             }
 
 
@@ -230,15 +264,15 @@ extension CoreDataStack {
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let success = batchDeleteResult.result as? Bool, success
                       let success = batchDeleteResult.result as? Bool, success
                 else {
                 else {
-                    debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-                    throw CoreDataError.batchDeleteError
+                    debug(.coreData, "Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
+                    throw CoreDataError.batchDeleteError(function: callingFunction, file: callingClass)
                 }
                 }
             }
             }
 
 
-            debugPrint("Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
+            debug(.coreData, "Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
         } catch {
         } catch {
-            debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
-            throw CoreDataError.batchDeleteError
+            debug(.coreData, "Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
         }
     }
     }
 
 
@@ -247,7 +281,9 @@ extension CoreDataStack {
         childType: Child.Type,
         childType: Child.Type,
         dateKey: String,
         dateKey: String,
         days: Int,
         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 {
     ) async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.name = "deleteContext"
@@ -267,7 +303,7 @@ extension CoreDataStack {
             }
             }
 
 
             guard !parentObjectIDs.isEmpty else {
             guard !parentObjectIDs.isEmpty else {
-//                debugPrint("No \(parentType) objects found older than \(days) days.")
+//                debug(.coreData,"No \(parentType) objects found older than \(days) days.")
                 return
                 return
             }
             }
 
 
@@ -281,7 +317,7 @@ extension CoreDataStack {
             }
             }
 
 
             guard !childObjectIDs.isEmpty else {
             guard !childObjectIDs.isEmpty else {
-//                debugPrint("No \(childType) objects found related to \(parentType) objects older than \(days) days.")
+//                debug(.coreData,"No \(childType) objects found related to \(parentType) objects older than \(days) days.")
                 return
                 return
             }
             }
 
 
@@ -292,17 +328,18 @@ extension CoreDataStack {
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let success = batchDeleteResult.result as? Bool, success
                       let success = batchDeleteResult.result as? Bool, success
                 else {
                 else {
-                    debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-                    throw CoreDataError.batchDeleteError
+                    debug(.coreData, "Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
+                    throw CoreDataError.batchDeleteError(function: callingFunction, file: callingClass)
                 }
                 }
             }
             }
 
 
-            debugPrint(
+            debug(
+                .coreData,
                 "Successfully deleted \(childType) data related to \(parentType) objects older than \(days) days. \(DebuggingIdentifiers.succeeded)"
                 "Successfully deleted \(childType) data related to \(parentType) objects older than \(days) days. \(DebuggingIdentifiers.succeeded)"
             )
             )
         } catch {
         } catch {
-            debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
-            throw CoreDataError.batchDeleteError
+            debug(.coreData, "Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
         }
     }
     }
 }
 }
@@ -323,7 +360,7 @@ extension CoreDataStack {
         propertiesToFetch: [String]? = nil,
         propertiesToFetch: [String]? = nil,
         callingFunction: String = #function,
         callingFunction: String = #function,
         callingClass: String = #fileID
         callingClass: String = #fileID
-    ) -> [Any] {
+    ) throws -> [Any] {
         let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: type))
         let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
         request.predicate = predicate
@@ -344,7 +381,7 @@ extension CoreDataStack {
         context.transactionAuthor = "fetchEntities"
         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
         /// 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 {
             do {
                 if propertiesToFetch != nil {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -352,11 +389,10 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                     return try context.fetch(request) as? [T] ?? []
                 }
                 }
             } catch let error as NSError {
             } 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 []
             }
             }
         }
         }
     }
     }
@@ -371,12 +407,14 @@ extension CoreDataStack {
         fetchLimit: Int? = nil,
         fetchLimit: Int? = nil,
         batchSize: Int? = nil,
         batchSize: Int? = nil,
         propertiesToFetch: [String]? = nil,
         propertiesToFetch: [String]? = nil,
+        relationshipKeyPathsForPrefetching: [String]? = nil,
         callingFunction: String = #function,
         callingFunction: String = #function,
         callingClass: String = #fileID
         callingClass: String = #fileID
-    ) async -> Any {
+    ) async throws -> Any {
         let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
         request.predicate = predicate
+
         if let limit = fetchLimit {
         if let limit = fetchLimit {
             request.fetchLimit = limit
             request.fetchLimit = limit
         }
         }
@@ -389,11 +427,14 @@ extension CoreDataStack {
         } else {
         } else {
             request.resultType = .managedObjectResultType
             request.resultType = .managedObjectResultType
         }
         }
+        if let prefetchKeyPaths = relationshipKeyPathsForPrefetching {
+            request.relationshipKeyPathsForPrefetching = prefetchKeyPaths
+        }
 
 
         context.name = "fetchContext"
         context.name = "fetchContext"
         context.transactionAuthor = "fetchEntities"
         context.transactionAuthor = "fetchEntities"
 
 
-        return await context.perform {
+        return try await context.perform {
             do {
             do {
                 if propertiesToFetch != nil {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -401,10 +442,11 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                     return try context.fetch(request) as? [T] ?? []
                 }
                 }
             } catch let error as NSError {
             } 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 []
             }
             }
         }
         }
     }
     }
@@ -412,9 +454,11 @@ extension CoreDataStack {
     // Get NSManagedObject
     // Get NSManagedObject
     func getNSManagedObject<T: NSManagedObject>(
     func getNSManagedObject<T: NSManagedObject>(
         with ids: [NSManagedObjectID],
         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]()
             var objects = [T]()
             do {
             do {
                 for id in ids {
                 for id in ids {
@@ -422,10 +466,13 @@ extension CoreDataStack {
                         objects.append(object)
                         objects.append(object)
                     }
                     }
                 }
                 }
+                return objects
             } catch {
             } catch {
-                debugPrint("Failed to fetch objects: \(error.localizedDescription)")
+                throw CoreDataError.fetchError(
+                    function: callingFunction,
+                    file: callingClass
+                )
             }
             }
-            return objects
         }
         }
     }
     }
 }
 }
@@ -442,7 +489,7 @@ extension CoreDataStack {
         do {
         do {
             try context.save()
             try context.save()
         } catch {
         } catch {
-            debugPrint("Error saving context \(DebuggingIdentifiers.failed): \(error)")
+            debug(.coreData, "Error saving context \(DebuggingIdentifiers.failed): \(error)")
         }
         }
     }
     }
 }
 }
@@ -458,11 +505,13 @@ extension NSManagedObjectContext {
         do {
         do {
             guard onContext.hasChanges else { return }
             guard onContext.hasChanges else { return }
             try onContext.save()
             try onContext.save()
-//            debugPrint(
-//                "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
-//            )
+            debug(
+                .coreData,
+                "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
+            )
         } catch let error as NSError {
         } catch let error as NSError {
-            debugPrint(
+            debug(
+                .coreData,
                 "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
                 "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
             )
             )
             throw error
             throw error

+ 24 - 21
Model/Helper/CoreDataError.swift

@@ -1,32 +1,35 @@
 import Foundation
 import Foundation
 
 
 enum CoreDataError: Error {
 enum CoreDataError: Error {
-    case creationError
-    case batchInsertError
-    case batchDeleteError
-    case persistentHistoryChangeError
-    case unexpectedError(error: Error)
-    case fetchError
-    case storeNotInitializedError
+    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)
+    case storeNotInitializedError(function: String, file: String)
 }
 }
 
 
 extension CoreDataError: LocalizedError {
 extension CoreDataError: LocalizedError {
     var errorDescription: String? {
     var errorDescription: String? {
         switch self {
         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 .storeNotInitializedError:
-            return NSLocalizedString("Failed to initialize Core Data's persistent store.", comment: "")
+        case let .creationError(function, file):
+            return String(localized: "Failed to create a new object in \(function) from \(file).")
+        case let .batchInsertError(function, file):
+            return String(localized: "Failed to execute a batch insert request in \(function) from \(file).")
+        case let .batchDeleteError(function, file):
+            return String(localized: "Failed to execute a batch delete request in \(function) from \(file).")
+        case let .persistentHistoryChangeError(function, file):
+            return String(localized: "Failed to execute a persistent history change request in \(function) from \(file).")
+        case let .unexpectedError(error, function, file):
+            return String(localized: "Received unexpected error in \(function) from \(file): \(error.localizedDescription)")
+        case let .fetchError(function, file):
+            return String(localized: "Failed to fetch object \(DebuggingIdentifiers.failed) in \(function) from \(file).")
+        case let .validationError(function, file):
+            return String(localized: "Failed to validate object in \(function) from \(file).")
+        case let .storeNotInitializedError(function, file):
+            return String(localized: "Store not initialized in \(function) from \(file).")
         }
         }
     }
     }
 }
 }

+ 21 - 0
Model/Helper/GlucoseStored+helper.swift

@@ -27,6 +27,27 @@ extension GlucoseStored {
 
 
         return glucose.allSatisfy { $0.glucose == firstValue }
         return glucose.allSatisfy { $0.glucose == firstValue }
     }
     }
+
+    // Preview
+    @discardableResult static func makePreviewGlucose(count: Int, provider: CoreDataStack) -> [GlucoseStored] {
+        let context = provider.persistentContainer.viewContext
+        let baseGlucose = 120
+        let glucoseValues = (0 ..< count).map { index -> GlucoseStored in
+            let glucose = GlucoseStored(context: context)
+            glucose.id = UUID()
+            glucose.date = Date.now.addingTimeInterval(Double(index) * -300) // Every 5 minutes
+            glucose.glucose = Int16(baseGlucose + (index % 3) * 10) // Varying between 120-140
+            glucose.direction = BloodGlucose.Direction.flat.rawValue
+            glucose.isManual = false
+            glucose.isUploadedToNS = false
+            glucose.isUploadedToHealth = false
+            glucose.isUploadedToTidepool = false
+            return glucose
+        }
+
+        try? context.save()
+        return glucoseValues
+    }
 }
 }
 
 
 extension NSPredicate {
 extension NSPredicate {

+ 31 - 1
Model/Helper/PumpEvent+helper.swift

@@ -12,6 +12,32 @@ extension PumpEventStored {
         }
         }
         return request
         return request
     }
     }
+
+    // Preview
+    @discardableResult static func makePreviewEvents(count: Int, provider: CoreDataStack) -> [PumpEventStored] {
+        let context = provider.persistentContainer.viewContext
+        let events = (0 ..< count).map { index -> PumpEventStored in
+            let event = PumpEventStored(context: context)
+            event.id = UUID().uuidString
+            event.timestamp = Date.now.addingTimeInterval(Double(index) * -300) // Every 5 minutes
+            event.type = EventType.bolus.rawValue
+            event.isUploadedToNS = false
+            event.isUploadedToHealth = false
+            event.isUploadedToTidepool = false
+
+            // Add a bolus
+            let bolus = BolusStored(context: context)
+            bolus.amount = 2.5 as NSDecimalNumber
+            bolus.isExternal = false
+            bolus.isSMB = false
+            bolus.pumpEvent = event
+
+            return event
+        }
+
+        try? context.save()
+        return events
+    }
 }
 }
 
 
 public extension PumpEventStored {
 public extension PumpEventStored {
@@ -65,7 +91,11 @@ extension NSPredicate {
 
 
     static var recentPumpHistory: NSPredicate {
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         let date = Date.twentyMinutesAgo
-        return NSPredicate(format: "timestamp >= %@", date as NSDate)
+        return NSPredicate(
+            format: "type == %@ AND timestamp <= %@",
+            PumpEventStored.EventType.tempBasal.rawValue,
+            date as NSDate
+        )
     }
     }
 
 
     static var lastPumpBolus: NSPredicate {
     static var lastPumpBolus: NSPredicate {

+ 11 - 0
Model/Helper/TempTargetStored+Helper.swift

@@ -14,6 +14,17 @@ extension NSPredicate {
             true as NSNumber
             true as NSNumber
         )
         )
     }
     }
+
+    static var tempTargetsForMainChart: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "(date >= %@ AND enabled == %@) OR (date >= %@ AND enabled == %@)",
+            date as NSDate,
+            true as NSNumber,
+            Date() as NSDate,
+            false as NSNumber
+        )
+    }
 }
 }
 
 
 extension TempTargetStored {
 extension TempTargetStored {

+ 16 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -51,6 +51,9 @@
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="descending"/>
             <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
         </fetchIndex>
+        <fetchIndex name="byValue">
+            <fetchIndexElement property="forecastValues" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     </entity>
     <entity name="ForecastValue" representedClassName="ForecastValue" syncable="YES">
     <entity name="ForecastValue" representedClassName="ForecastValue" syncable="YES">
         <attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@@ -140,6 +143,9 @@
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
         </fetchIndex>
         </fetchIndex>
+        <fetchIndex name="byTimestamp">
+            <fetchIndexElement property="timestamp" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     </entity>
     <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
     <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
         <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@@ -205,6 +211,15 @@
     <entity name="StatsData" representedClassName="StatsData" syncable="YES">
     <entity name="StatsData" representedClassName="StatsData" syncable="YES">
         <attribute name="lastrun" attributeType="Date" defaultDateTimeInterval="704497620" usesScalarValueType="NO"/>
         <attribute name="lastrun" attributeType="Date" defaultDateTimeInterval="704497620" usesScalarValueType="NO"/>
     </entity>
     </entity>
+    <entity name="TDDStored" representedClassName="TDDStored" syncable="YES">
+        <attribute name="bolus" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="scheduledBasal" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="tempBasal" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="total" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="weightedAverage" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+    </entity>
     <entity name="TempBasalStored" representedClassName="TempBasalStored" syncable="YES">
     <entity name="TempBasalStored" representedClassName="TempBasalStored" syncable="YES">
         <attribute name="duration" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="duration" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="rate" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="rate" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>

+ 4 - 0
TDDStored+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(TDDStored) public class TDDStored: NSManagedObject {}

+ 18 - 0
TDDStored+CoreDataProperties.swift

@@ -0,0 +1,18 @@
+import CoreData
+import Foundation
+
+public extension TDDStored {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<TDDStored> {
+        NSFetchRequest<TDDStored>(entityName: "TDDStored")
+    }
+
+    @NSManaged var id: UUID?
+    @NSManaged var date: Date?
+    @NSManaged var total: NSDecimalNumber?
+    @NSManaged var bolus: NSDecimalNumber?
+    @NSManaged var tempBasal: NSDecimalNumber?
+    @NSManaged var scheduledBasal: NSDecimalNumber?
+    @NSManaged var weightedAverage: NSDecimalNumber?
+}
+
+extension TDDStored: Identifiable {}

+ 11 - 0
Trio Watch App Extension/Views/AcknowledgementPendingView.swift

@@ -43,6 +43,17 @@ struct AcknowledgementPendingView: View {
         .navigationBarBackButtonHidden(true)
         .navigationBarBackButtonHidden(true)
         .toolbar(.hidden)
         .toolbar(.hidden)
         .background(trioBackgroundColor)
         .background(trioBackgroundColor)
+        .onChange(of: state.showCommsAnimation) { oldValue, newValue in
+            if newValue && !oldValue {
+                DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
+                    // If after 5 seconds there is still no acknowledgement banner, return to root
+                    if !state.showAcknowledgmentBanner {
+                        // Navigate back to the root
+                        navigationPath.removeLast(navigationPath.count)
+                    }
+                }
+            }
+        }
         .onChange(of: state.showAcknowledgmentBanner) { _, newValue in
         .onChange(of: state.showAcknowledgmentBanner) { _, newValue in
             if !newValue {
             if !newValue {
                 // Navigate back to the root when acknowledgment banner disappears
                 // Navigate back to the root when acknowledgment banner disappears

+ 1 - 1
Trio Watch App Extension/Views/BolusConfirmationView.swift

@@ -37,7 +37,7 @@ struct BolusConfirmationView: View {
                 HStack {
                 HStack {
                     Text("Bolus")
                     Text("Bolus")
                     Spacer()
                     Spacer()
-                    Text(String(format: "%.2f U", adjustedBolusAmount))
+                    Text(String(format: "%.2f \(String(localized: "U", comment: "Insulin unit"))", adjustedBolusAmount))
                         .bold()
                         .bold()
                         .foregroundStyle(Color.insulin)
                         .foregroundStyle(Color.insulin)
                 }.padding(.horizontal)
                 }.padding(.horizontal)

+ 9 - 3
Trio Watch App Extension/Views/BolusInputView.swift

@@ -25,7 +25,10 @@ struct BolusInputView: View {
     var body: some View {
     var body: some View {
         VStack {
         VStack {
             if state.showBolusCalculationProgress {
             if state.showBolusCalculationProgress {
-                ProgressView("Calculating Bolus...")
+                ProgressView(String(
+                    localized: "Calculating Bolus...",
+                    comment: "Progress view text on watch when calculating bolus"
+                ))
                 Spacer()
                 Spacer()
             } else {
             } else {
                 if effectiveBolusLimit <= 0 {
                 if effectiveBolusLimit <= 0 {
@@ -63,7 +66,7 @@ struct BolusInputView: View {
                         let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
                         let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
                         let adjustedBolusAmount = floor(bolusAmount / bolusIncrement) * bolusIncrement
                         let adjustedBolusAmount = floor(bolusAmount / bolusIncrement) * bolusIncrement
 
 
-                        Text(String(format: "%.2f U", adjustedBolusAmount))
+                        Text(String(format: "%.2f \(String(localized: "U", comment: "Insulin unit"))", adjustedBolusAmount))
                             .fontWeight(.bold)
                             .fontWeight(.bold)
                             .font(.system(.title2, design: .rounded))
                             .font(.system(.title2, design: .rounded))
                             .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
                             .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
@@ -117,7 +120,10 @@ struct BolusInputView: View {
                     .tint(Color.insulin)
                     .tint(Color.insulin)
                     .disabled(!(bolusAmount > 0.0) || bolusAmount > effectiveBolusLimit)
                     .disabled(!(bolusAmount > 0.0) || bolusAmount > effectiveBolusLimit)
 
 
-                    Text(String(format: "Recommended: %.1f U", NSDecimalNumber(decimal: state.recommendedBolus).doubleValue))
+                    Text(String(
+                        format: "\(String(localized: "Recommended:", comment: "Recommended bolus on Watch")) %.1f \(String(localized: "U", comment: "Insulin unit"))",
+                        NSDecimalNumber(decimal: state.recommendedBolus).doubleValue
+                    ))
                         .font(.footnote)
                         .font(.footnote)
                         .foregroundStyle(.secondary)
                         .foregroundStyle(.secondary)
                 }
                 }

+ 4 - 1
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -47,7 +47,10 @@ struct BolusProgressOverlay: View {
                     .tint(progressGradient)
                     .tint(progressGradient)
 
 
                 Text(String(
                 Text(String(
-                    format: "%.2f U of %.2f U",
+                    format: String(
+                        localized: "%.2f U of %.2f U",
+                        comment: "Format for showing delivered and active bolus amounts, 'x U of y U' on watch"
+                    ),
                     state.deliveredAmount,
                     state.deliveredAmount,
                     state.activeBolusAmount
                     state.activeBolusAmount
                 ))
                 ))

+ 3 - 2
Trio Watch App Extension/Views/CarbsInputView.swift

@@ -22,7 +22,8 @@ struct CarbsInputView: View {
     )
     )
 
 
     var body: some View {
     var body: some View {
-        let buttonLabel = continueToBolus ? "Proceed" : "Log Carbs"
+        let buttonLabel = continueToBolus ? String(localized: "Proceed", comment: "Button Label to Proceed to Bolus on Watch") :
+            String(localized: "Log Carbs", comment: "Button Label to Log Carbs on Watch")
 
 
         // TODO: introduce meal setting fpu enablement to conditional handle FPU
         // TODO: introduce meal setting fpu enablement to conditional handle FPU
         VStack {
         VStack {
@@ -45,7 +46,7 @@ struct CarbsInputView: View {
                 Spacer()
                 Spacer()
 
 
                 // Display the current carb amount
                 // Display the current carb amount
-                Text(String(format: "%.0f g", carbsAmount))
+                Text(String(format: "%.0f \(String(localized: "g", comment: "gram of carbs"))", carbsAmount))
                     .fontWeight(.bold)
                     .fontWeight(.bold)
                     .font(.system(.title2, design: .rounded))
                     .font(.system(.title2, design: .rounded))
                     .foregroundColor(carbsAmount > 0.0 && carbsAmount >= effectiveCarbsLimit ? .loopRed : .primary)
                     .foregroundColor(carbsAmount > 0.0 && carbsAmount >= effectiveCarbsLimit ? .loopRed : .primary)

+ 8 - 3
Trio Watch App Extension/Views/GlucoseTrendView.swift

@@ -148,9 +148,14 @@ struct GlucoseTrendView: View {
 
 
             Spacer()
             Spacer()
 
 
-            Text(isWatchStateDated ? "STALE DATA" : state.lastLoopTime ?? "--")
-                .font(.system(size: minutesAgoFontSize))
-                .fontWidth(isWatchStateDated ? .expanded : .standard)
+            Text(
+                isWatchStateDated ?
+                    String(localized: "STALE DATA", comment: "Information displayed when watch app data outdated or stale.") :
+                    state
+                    .lastLoopTime ?? "--"
+            )
+            .font(.system(size: minutesAgoFontSize))
+            .fontWidth(isWatchStateDated ? .expanded : .standard)
 
 
             Spacer()
             Spacer()
 
 

+ 3 - 3
Trio Watch App Extension/Views/TreatmentMenuView.swift

@@ -103,9 +103,9 @@ enum TreatmentOption: String, CaseIterable, Identifiable {
 
 
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
-        case .mealBolusCombo: return "Meal & Bolus"
-        case .meal: return "Meal"
-        case .bolus: return "Bolus"
+        case .mealBolusCombo: return String(localized: "Meal & Bolus", comment: "Watch App Treatment Option 'Meal & Bolus'")
+        case .meal: return String(localized: "Meal", comment: "Watch App Treatment Option 'Meal'")
+        case .bolus: return String(localized: "Bolus", comment: "Watch App Treatment Option 'Bolus'")
         }
         }
     }
     }
 }
 }

+ 3 - 5
Trio Watch App Extension/WatchState.swift

@@ -110,15 +110,13 @@ import WatchConnectivity
             acknowledgmentMessage = "\(message)"
             acknowledgmentMessage = "\(message)"
         } else {
         } else {
             print("⌚️ Acknowledgment failed: \(message)")
             print("⌚️ Acknowledgment failed: \(message)")
+            DispatchQueue.main.async {
+                self.showCommsAnimation = false // Hide progress animation
+            }
             acknowledgementStatus = .failure
             acknowledgementStatus = .failure
             acknowledgmentMessage = "\(message)"
             acknowledgmentMessage = "\(message)"
         }
         }
 
 
-        DispatchQueue.main.async {
-            self.showCommsAnimation = false // Hide progress animation
-            self.showSyncingAnimation = false // Just ensure this is 100% set to false
-        }
-
         if isFinal {
         if isFinal {
             showAcknowledgmentBanner = true
             showAcknowledgmentBanner = true
             DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
             DispatchQueue.main.asyncAfter(deadline: .now() + 2) {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 64 - 151
Trio.xcodeproj/project.pbxproj


+ 1 - 1
Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch App.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
 <Scheme
-   LastUpgradeVersion = "1600"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    version = "1.7">
    <BuildAction
    <BuildAction
       parallelizeBuildables = "YES"
       parallelizeBuildables = "YES"

+ 1 - 1
Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch Complication Extension.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
 <Scheme
-   LastUpgradeVersion = "1600"
+   LastUpgradeVersion = "1620"
    version = "2.0">
    version = "2.0">
    <BuildAction
    <BuildAction
       parallelizeBuildables = "YES"
       parallelizeBuildables = "YES"

+ 2 - 2
Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
 <Scheme
-   LastUpgradeVersion = "1240"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    version = "1.3">
    <BuildAction
    <BuildAction
-      parallelizeBuildables = "NO"
+      parallelizeBuildables = "YES"
       buildImplicitDependencies = "YES">
       buildImplicitDependencies = "YES">
       <BuildActionEntries>
       <BuildActionEntries>
          <BuildActionEntry
          <BuildActionEntry

+ 0 - 23
Trio/Resources/Base.lproj/InfoPlist.strings

@@ -1,23 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Contacts Usage Description */
-"NSContactsUsageDescription" = "Allows Trio to access your contacts for live updates to your Apple Watch contact complication using the 'Contact Trick' feature.";

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1059 - 0
Trio/Resources/InfoPlist.xcstrings


+ 0 - 20
Trio/Resources/ar.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, carbs and insulin";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, carbs and insulin";

+ 0 - 20
Trio/Resources/ca.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/da.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC bruges til at scanne Libre sensorer.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth bliver brugt til at kommunikere med din insulin pumpe og dine glukose monitor enheder";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth bliver brugt til at kommunikere med din insulin pumpe og dine glukose monitor enheder";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For autoriseret adgang til bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Kalender bruges til at oprette en ny glucose begivenheder.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App bruges til at opbevare blodglukose, insulin og kulhydrater";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App bruges til at opbevare blodglukose, insulin og kulhydrater";

+ 0 - 20
Trio/Resources/de.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC wird zum Scannen von Libre Sensoren benutzt.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth wird verwendet, um mit Insulinpumpen und CGMs zu kommunizieren.";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth wird verwendet, um mit Insulinpumpen und CGMs zu kommunizieren.";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Autorisierung für Bolusabgabe";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "BZ-Werte werden im Kalender als temporärer Termin erstellt.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Die Apple Health App wird zum Speichern von Blutzuckerwerten, Insulin und Kohlenhydraten verwendet";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Die Apple Health App wird zum Speichern von Blutzuckerwerten, Insulin und Kohlenhydraten verwendet";

+ 0 - 20
Trio/Resources/es.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/fi.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/fr.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC est utilisé pour scanner les capteurs Libre.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth est utilisé pour communiquer avec la pompe à insuline et les dispositifs de surveillance continue du glucose";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth est utilisé pour communiquer avec la pompe à insuline et les dispositifs de surveillance continue du glucose";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Pour les accès autorisés au bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Le calendrier est utilisé pour créer un nouvel événement de glycémie.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "L'application Santé est utilisée pour stocker la glycémie, l'insuline et les glucides";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "L'application Santé est utilisée pour stocker la glycémie, l'insuline et les glucides";

+ 0 - 20
Trio/Resources/he.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/hu.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/it.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC è usato per scansionare i sensori di Libre.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Il bluetooth viene utilizzato per comunicare con il microinfusore e i dispositivi CGM";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Il Bluetooth viene utilizzato per comunicare con il microinfusore e i dispositivi CGM";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Per accesso autorizzato al bolo";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Il calendario è usato per creare nuovi eventi di glicemia.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "L'app Salute è usata per memorizzare glicemie, insulina e carboidrati";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "L'app Salute è usata per memorizzare glicemie, insulina e carboidrati";

+ 0 - 20
Trio/Resources/nb.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC brukes til å skanne Libre-sensorer.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth brukes til å kommunisere med insulinpumpe og blodsukkersensor";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth brukes til å kommunisere med insulinpumpe og blodsukkersensor-enheter";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For autorisert tilgang til bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Kalender brukes til å opprette nye blodsukker-oppføringer.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Helse-appen brukes til å lagre blodsukker, insulin og karbohydrater";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Helse-appen brukes til å lagre blodsukker, insulin og karbohydrater";

+ 0 - 20
Trio/Resources/nl.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC wordt gebruikt voor het scannen van Libre sensoren.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth wordt gebruikt om te communiceren met de insuline pomp en de continue glucose meter";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth wordt gebruikt om te communiceren met de insuline pomp en de continue glucose meter";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Voor geautoriseerde toegang tot bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Agenda wordt gebruikt om nieuwe glucose gebeurtenissen aan te maken.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Apple Gezondheid wordt gebruikt om bloedglucose, insuline en koolhydraten op te slaan";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Apple Gezondheid wordt gebruikt om bloedglucose, insuline en koolhydraten op te slaan";

+ 0 - 20
Trio/Resources/pl.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/pt-BR.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/pt-PT.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/ru.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC используется для сканирования сенсоров Libre.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth используется для связи с инсулиновой помпой и устройствами непрерывного мониторинга глюкозы";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth используется для связи с инсулиновой помпой и устройствами непрерывного мониторинга глюкозы";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Для авторизованного болюса";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Календарь используется для создания новых событий о глюкозе.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Приложение здоровья используется для хранения глюкозы, инсулина и углеводов в крови";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Приложение здоровья используется для хранения глюкозы, инсулина и углеводов в крови";

+ 0 - 20
Trio/Resources/sk.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC sa používa na skenovanie snímačov Libre.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth sa používa pre komunikáciu s inzulínovou pumpou a zariadeniami na kontinuálne monitorovanie krvného cukru";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth sa používa pre komunikáciu s inzulínovou pumpou a zariadeniami na kontinuálne monitorovanie krvného cukru";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Pre autorizovaný prístup k bolusu";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Kalendár slúži na vytvorenie novej udalosti s glukózou.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Aplikácia Zdravie sa používa na meranie glukózy v krvi, inzulínu a ukladanie sacharidov";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Aplikácia Zdravie sa používa na meranie glukózy v krvi, inzulínu a ukladanie sacharidov";

+ 0 - 20
Trio/Resources/sv.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC används för att skanna Libre-sensorn vid uppstart.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth används för att kommunicera med pump och kontinuerlig glukosmätare";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth används för att kommunicera med insulinpumpen och kontinuerliga glukosmätare";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "För auktoriserad åtkomst till bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Kalendern används för att skapa kalenderhändelser för glukosvärden.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Appen Hälsa används för att lagra blodsockervärden, insulin samt kolhydrater";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Appen Hälsa används för att lagra blodsockervärden, insulin och kolhydrater.";

+ 0 - 20
Trio/Resources/tr.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC Libre sensörlerini taramak için kullanılır.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth, insülin pompası ve sürekli glikoz izleme cihazları ile iletişim kurmak için kullanılır";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth, insülin pompası ve sürekli glikoz izleme cihazları ile iletişim kurmak için kullanılır";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Bolus'a yetkili erişim için";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Takvim, yeni bir glikoz olayı oluşturmak için kullanılır.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 0 - 20
Trio/Resources/uk.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC використовується для сканування сенсорів Libre.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth використовується для обміну з інсуліновими помпами та безперервним моніторингом глюкози";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth використовується для обміну з інсуліновими помпами та безперервним моніторингом глюкози";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Для авторизованого болюсу";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Для створення нових подій глюкози використовується календар.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Додаток Health використовується для зберігання глюкози в крові, інсуліну та вуглеводів";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Додаток Health використовується для зберігання глюкози в крові, інсуліну та вуглеводів";

+ 0 - 20
Trio/Resources/vi.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC được sử dụng để quét các cảm biến Libre.";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth được sử dụng để liên lạc với máy bơm insulin và các thiết bị theo dõi đường huyết liên tục/CGM.";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth được sử dụng để liên lạc với máy bơm insulin và các thiết bị theo dõi đường huyết liên tục/CGM.";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "Được cấp quyền truy cập vào bolus";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Lịch \nCalendar được sử dụng để tạo ra một sự kiện glucose mới.";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Ứng dụng sức khỏe \nHealth được sử dụng để lưu trữ đường huyết, insulin và carbohydrate";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Ứng dụng sức khỏe \nHealth được sử dụng để lưu trữ đường huyết, insulin và carbohydrate";

+ 0 - 20
Trio/Resources/zh-Hans.lproj/InfoPlist.strings

@@ -1,20 +0,0 @@
-/* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC 用于扫描 Libre 传感器。";
-
-/* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "蓝牙用于与胰岛素泵和连续血糖监测设备进行通信";
-
-/* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "蓝牙用于与胰岛素泵和连续血糖监测设备进行通信";
-
-/* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "用于输注胰岛素授权";
-
-/* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "日历用于创建一个新的葡萄糖事件。";
-
-/* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
-
-/* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";

+ 352 - 321
Trio/Sources/APS/APSManager.swift

@@ -20,8 +20,8 @@ protocol APSManager {
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     var isManualTempBasal: Bool { get }
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func enactTempBasal(rate: Double, duration: TimeInterval) async
-    func determineBasal() async -> Bool
-    func determineBasalSync() async
+    func determineBasal() async throws
+    func determineBasalSync() async throws
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     var lastError: CurrentValueSubject<Error?, Never> { get }
@@ -65,6 +65,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var deviceDataManager: DeviceDataManager!
     @Injected() private var deviceDataManager: DeviceDataManager!
     @Injected() private var nightscout: NightscoutManager!
     @Injected() private var nightscout: NightscoutManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var tddStorage: TDDStorage!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Persisted(key: "lastLoopStartDate") private var lastLoopStartDate: Date = .distantPast
     @Persisted(key: "lastLoopStartDate") private var lastLoopStartDate: Date = .distantPast
     @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
     @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
@@ -130,7 +131,14 @@ final class BaseAPSManager: APSManager, Injectable {
             let wasParsed = storage.parseOnFileSettingsToMgdL()
             let wasParsed = storage.parseOnFileSettingsToMgdL()
             if wasParsed {
             if wasParsed {
                 Task {
                 Task {
-                    await openAPS.createProfiles()
+                    do {
+                        try await openAPS.createProfiles()
+                    } catch {
+                        debug(
+                            .apsManager,
+                            "\(DebuggingIdentifiers.failed) Error creating profiles: \(error.localizedDescription)"
+                        )
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -184,106 +192,134 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     // Loop entry point
     // Loop entry point
     private func loop() {
     private func loop() {
-        Task {
-            // check the last start of looping is more the loopInterval but the previous loop was completed
-            if lastLoopDate > lastLoopStartDate {
-                guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
-                    debug(.apsManager, "too close to do a loop : \(lastLoopStartDate)")
-                    return
+        Task { [weak self] in
+            guard let self else { return }
+
+            // Check if we can start a new loop
+            guard await self.canStartNewLoop() else { return }
+
+            // Setup loop and background task
+            var (loopStatRecord, backgroundTask) = await self.setupLoop()
+
+            do {
+                // Execute loop logic
+                try await self.executeLoop(loopStatRecord: &loopStatRecord)
+
+                // Upload data to Nightscout if available
+                if let nightscoutManager = self.nightscout {
+                    await nightscoutManager.uploadCarbs()
+                    await nightscoutManager.uploadPumpHistory()
+                    await nightscoutManager.uploadOverrides()
+                    await nightscoutManager.uploadTempTargets()
                 }
                 }
+            } catch {
+                var updatedStats = loopStatRecord
+                updatedStats.end = Date()
+                updatedStats.duration = roundDouble((updatedStats.end! - updatedStats.start).timeInterval / 60, 2)
+                updatedStats.loopStatus = error.localizedDescription
+                await loopCompleted(error: error, loopStatRecord: updatedStats)
+                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to complete Loop: \(error.localizedDescription)")
             }
             }
 
 
-            guard !isLooping.value else {
-                warning(.apsManager, "Loop already in progress. Skip recommendation.")
-                return
+            // Cleanup background task
+            if let backgroundTask = backgroundTask {
+                await UIApplication.shared.endBackgroundTask(backgroundTask)
+                self.backGroundTaskID = .invalid
             }
             }
+        }
+    }
 
 
-            // start background time extension
-            backGroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
-                guard let backgroundTask = self.backGroundTaskID else { return }
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTask)
-                }
-                self.backGroundTaskID = .invalid
+    private func canStartNewLoop() async -> Bool {
+        // Check if too soon for next loop
+        if lastLoopDate > lastLoopStartDate {
+            guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
+                debug(.apsManager, "Not enough time have passed since last loop at : \(lastLoopStartDate)")
+                return false
             }
             }
+        }
 
 
-            lastLoopStartDate = Date()
+        // Check if loop already in progress
+        guard !isLooping.value else {
+            warning(.apsManager, "Loop already in progress. Skip recommendation.")
+            return false
+        }
 
 
-            var previousLoop = [LoopStatRecord]()
-            var interval: Double?
+        return true
+    }
 
 
-            do {
-                try await privateContext.perform {
-                    let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
-                    let sortStats = NSSortDescriptor(key: "end", ascending: false)
-                    requestStats.sortDescriptors = [sortStats]
-                    requestStats.fetchLimit = 1
-                    previousLoop = try self.privateContext.fetch(requestStats)
-
-                    if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
-                        interval = self.roundDouble(
-                            (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
-                            1
-                        )
-                    }
-                }
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error.userInfo)"
-                )
+    private func setupLoop() async -> (LoopStats, UIBackgroundTaskIdentifier?) {
+        // Start background task
+        let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") { [weak self] in
+            guard let self, let backgroundTask = self.backGroundTaskID else { return }
+            Task {
+                UIApplication.shared.endBackgroundTask(backgroundTask)
             }
             }
+            self.backGroundTaskID = .invalid
+        }
+        backGroundTaskID = backgroundTask
 
 
-            var loopStatRecord = LoopStats(
-                start: lastLoopStartDate,
-                loopStatus: "Starting",
-                interval: interval
-            )
+        // Set loop start time
+        lastLoopStartDate = Date()
 
 
-            isLooping.send(true)
+        // Calculate interval from previous loop
+        let interval = await calculateLoopInterval()
 
 
-            do {
-                if await !determineBasal() {
-                    throw APSError.apsError(message: "Determine basal failed")
-                }
+        // Create initial loop stats record
+        let loopStatRecord = LoopStats(
+            start: lastLoopStartDate,
+            loopStatus: "Starting",
+            interval: interval
+        )
 
 
-                // Open loop completed
-                guard settings.closedLoop else {
-                    loopStatRecord.end = Date()
-                    loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                    loopStatRecord.loopStatus = "Success"
-                    await loopCompleted(loopStatRecord: loopStatRecord)
-                    return
-                }
+        isLooping.send(true)
 
 
-                // Closed loop - enact Determination
-                try await enactDetermination()
-                loopStatRecord.end = Date()
-                loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                loopStatRecord.loopStatus = "Success"
-                await loopCompleted(loopStatRecord: loopStatRecord)
-            } catch {
-                loopStatRecord.end = Date()
-                loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                loopStatRecord.loopStatus = error.localizedDescription
-                await loopCompleted(error: error, loopStatRecord: loopStatRecord)
-            }
+        return (loopStatRecord, backgroundTask)
+    }
 
 
-            if let nightscoutManager = nightscout {
-                await nightscoutManager.uploadCarbs()
-                await nightscoutManager.uploadPumpHistory()
-                await nightscoutManager.uploadOverrides()
-                await nightscoutManager.uploadTempTargets()
-            }
+    private func executeLoop(loopStatRecord: inout LoopStats) async throws {
+        try await determineBasal()
 
 
-            // End background task after all the operations are completed
-            if let backgroundTask = self.backGroundTaskID {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
-                self.backGroundTaskID = .invalid
+        // Handle open loop
+        guard settings.closedLoop else {
+            loopStatRecord.end = Date()
+            loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+            loopStatRecord.loopStatus = "Success"
+            await loopCompleted(loopStatRecord: loopStatRecord)
+            return
+        }
+
+        // Handle closed loop
+        try await enactDetermination()
+        loopStatRecord.end = Date()
+        loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+        loopStatRecord.loopStatus = "Success"
+        await loopCompleted(loopStatRecord: loopStatRecord)
+    }
+
+    private func calculateLoopInterval() async -> Double? {
+        do {
+            return try await privateContext.perform {
+                let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
+                let sortStats = NSSortDescriptor(key: "end", ascending: false)
+                requestStats.sortDescriptors = [sortStats]
+                requestStats.fetchLimit = 1
+                let previousLoop = try self.privateContext.fetch(requestStats)
+
+                if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
+                    return self.roundDouble(
+                        (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
+                        1
+                    )
+                }
+                return nil
             }
             }
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error)")
+            return nil
         }
         }
     }
     }
 
 
-//     Loop exit point
+    // Loop exit point
     private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
     private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
         isLooping.send(false)
         isLooping.send(false)
 
 
@@ -346,14 +382,39 @@ final class BaseAPSManager: APSManager, Injectable {
         return false
         return false
     }
     }
 
 
-    func determineBasal() async -> Bool {
+    /// Calculates and stores the Total Daily Dose (TDD)
+    private func calculateAndStoreTDD() async throws {
+        guard let pumpManager else { return }
+
+        async let pumpHistory = pumpHistoryStorage.getPumpHistory()
+        async let basalProfile = storage
+            .retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) ??
+            [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile)) ??
+            [] // OpenAPS.defaults ensures we at least get default rate of 1u/hr for 24 hrs
+
+        // Calculate TDD
+        let tddResult = try await tddStorage.calculateTDD(
+            pumpManager: pumpManager,
+            pumpHistory: pumpHistory,
+            basalProfile: basalProfile
+        )
+
+        // Store TDD in Core Data
+        await tddStorage.storeTDD(tddResult)
+    }
+
+    func determineBasal() async throws {
         debug(.apsManager, "Start determine basal")
         debug(.apsManager, "Start determine basal")
 
 
+        try await calculateAndStoreTDD()
+
         // Fetch glucose asynchronously
         // 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
         // Perform the context-related checks and actions
-        let isValidGlucoseData = await privateContext.perform {
+        let isValidGlucoseData = await privateContext.perform { [weak self] in
+            guard let self else { return false }
+
             guard glucose.count > 2 else {
             guard glucose.count > 2 else {
                 debug(.apsManager, "Not enough glucose data")
                 debug(.apsManager, "Not enough glucose data")
                 self.processError(APSError.glucoseError(message: "Not enough glucose data"))
                 self.processError(APSError.glucoseError(message: "Not enough glucose data"))
@@ -379,7 +440,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard isValidGlucoseData else {
         guard isValidGlucoseData else {
             debug(.apsManager, "Glucose validation failed")
             debug(.apsManager, "Glucose validation failed")
             processError(APSError.glucoseError(message: "Glucose validation failed"))
             processError(APSError.glucoseError(message: "Glucose validation failed"))
-            return false
+            return
         }
         }
 
 
         do {
         do {
@@ -390,32 +451,30 @@ final class BaseAPSManager: APSManager, Injectable {
             async let autosenseResult = autosense()
             async let autosenseResult = autosense()
 
 
             _ = try await autosenseResult
             _ = try await autosenseResult
-            await openAPS.createProfiles()
+            try await openAPS.createProfiles()
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
 
 
             if let determination = determination {
             if let determination = determination {
-                DispatchQueue.main.async {
+                // Capture weak self in closure
+                await MainActor.run { [weak self] in
+                    guard let self else { return }
                     self.broadcaster.notify(DeterminationObserver.self, on: .main) {
                     self.broadcaster.notify(DeterminationObserver.self, on: .main) {
                         $0.determinationDidUpdate(determination)
                         $0.determinationDidUpdate(determination)
                     }
                     }
                 }
                 }
-                return true
-            } else {
-                return false
             }
             }
         } catch {
         } catch {
-            debug(.apsManager, "Error determining basal: \(error)")
-            return false
+            throw APSError.apsError(message: "Error determining basal: \(error.localizedDescription)")
         }
         }
     }
     }
 
 
-    func determineBasalSync() async {
-        _ = await determineBasal()
+    func determineBasalSync() async throws {
+        _ = try await determineBasal()
     }
     }
 
 
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
         do {
         do {
-            let temp = await fetchCurrentTempBasal(date: Date.now)
+            let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
             return try await openAPS.determineBasal(
                 currentTemp: temp,
                 currentTemp: temp,
                 clock: Date(),
                 clock: Date(),
@@ -447,12 +506,14 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
         if let error = verifyStatus() {
         if let error = verifyStatus() {
             processError(error)
             processError(error)
-            processQueue.async {
-                self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+            // Capture broadcaster and queue before async context
+            let broadcaster = self.broadcaster
+            Task { @MainActor in
+                broadcaster?.notify(BolusFailureObserver.self, on: .main) {
                     $0.bolusDidFail()
                     $0.bolusDidFail()
                 }
                 }
             }
             }
-            callback?(false, "Error! Failed to enact bolus.")
+            callback?(false, String(localized: "Error! Failed to enact bolus.", comment: "Error message for enacting a bolus"))
             return
             return
         }
         }
 
 
@@ -466,21 +527,26 @@ final class BaseAPSManager: APSManager, Injectable {
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             debug(.apsManager, "Bolus succeeded")
             debug(.apsManager, "Bolus succeeded")
             if !isSMB {
             if !isSMB {
-                await determineBasalSync()
+                try await determineBasalSync()
             }
             }
             bolusProgress.send(0)
             bolusProgress.send(0)
-            callback?(true, "Bolus enacted successfully.")
+            callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
         } catch {
         } catch {
             warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             processError(APSError.pumpError(error))
             processError(APSError.pumpError(error))
             if !isSMB {
             if !isSMB {
-                processQueue.async {
-                    self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+                // Use MainActor to handle broadcaster notification
+                let broadcaster = self.broadcaster
+                Task { @MainActor in
+                    broadcaster?.notify(BolusFailureObserver.self, on: .main) {
                         $0.bolusDidFail()
                         $0.bolusDidFail()
                     }
                     }
                 }
                 }
             }
             }
-            callback?(false, "Error! Failed to enact bolus.")
+            callback?(
+                false,
+                String(localized: "Error! Failed to enact bolus.", comment: "Error message for failing to enact a bolus")
+            )
         }
         }
     }
     }
 
 
@@ -490,11 +556,14 @@ final class BaseAPSManager: APSManager, Injectable {
         do {
         do {
             _ = try await pump.cancelBolus()
             _ = try await pump.cancelBolus()
             debug(.apsManager, "Bolus cancelled")
             debug(.apsManager, "Bolus cancelled")
-            callback?(true, "Bolus cancelled successfully.")
+            callback?(true, String(localized: "Bolus cancelled successfully.", comment: "Success message for canceling a bolus"))
         } catch {
         } catch {
             debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
             debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
             processError(APSError.pumpError(error))
             processError(APSError.pumpError(error))
-            callback?(false, "Error! Bolus cancellation failed.")
+            callback?(
+                false,
+                String(localized: "Error! Bolus cancellation failed.", comment: "Error message for canceling a bolus")
+            )
         }
         }
         bolusReporter?.removeObserver(self)
         bolusReporter?.removeObserver(self)
         bolusReporter = nil
         bolusReporter = nil
@@ -528,8 +597,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,
             ofType: PumpEventStored.self,
             onContext: privateContext,
             onContext: privateContext,
             predicate: NSPredicate.recentPumpHistory,
             predicate: NSPredicate.recentPumpHistory,
@@ -568,7 +637,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     private func enactDetermination() async throws {
     private func enactDetermination() async throws {
-        guard let determinationID = await determinationStorage
+        guard let determinationID = try await determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
             .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
         else {
         else {
             throw APSError.apsError(message: "Determination not found")
             throw APSError.apsError(message: "Determination not found")
@@ -623,35 +692,39 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     private func reportEnacted(wasEnacted: Bool) async {
     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.timestamp = Date()
                 determinationUpdated.enacted = wasEnacted
                 determinationUpdated.enacted = wasEnacted
                 determinationUpdated.isUploadedToNS = false
                 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)")
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
 
 
                 Task.detached(priority: .low) {
                 Task.detached(priority: .low) {
                     await self.statistics()
                     await self.statistics()
                 }
                 }
-            } else {
-                debugPrint("Failed to update OrefDetermination in reportEnacted()")
             }
             }
+        } catch {
+            debug(
+                .apsManager,
+                "\(DebuggingIdentifiers.failed) Error reporting enacted status: \(error.localizedDescription)"
+            )
         }
         }
     }
     }
 
 
@@ -811,8 +884,8 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     // fetch glucose for time interval
     // 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,
             ofType: GlucoseStored.self,
             onContext: privateContext,
             onContext: privateContext,
             predicate: predicate,
             predicate: predicate,
@@ -822,9 +895,9 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
             batchSize: batchSize
         )
         )
 
 
-        return await privateContext.perform {
+        return try await privateContext.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return glucoseResults
             return glucoseResults
@@ -849,7 +922,7 @@ final class BaseAPSManager: APSManager, Injectable {
             async let carbTotal = carbsForStats()
             async let carbTotal = carbsForStats()
             async let preferences = settingsManager.preferences
             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
             // Only save and upload once per day
             guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
             guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
@@ -872,31 +945,11 @@ final class BaseAPSManager: APSManager, Injectable {
             }
             }
             let af = pref.adjustmentFactor
             let af = pref.adjustmentFactor
             let insulin_type = pref.curve
             let insulin_type = pref.curve
-//            let buildDate = Bundle.main.buildDate // TODO: fix this
+            let buildDate = BuildDetails.default.buildDate()
             let version = Bundle.main.releaseVersionNumber
             let version = Bundle.main.releaseVersionNumber
             let build = Bundle.main.buildVersionNumber
             let build = Bundle.main.buildVersionNumber
 
 
-            // Read branch information from branch.txt instead of infoDictionary
-            var branch = "Unknown"
-            if let branchFileURL = Bundle.main.url(forResource: "branch", withExtension: "txt"),
-               let branchFileContent = try? String(contentsOf: branchFileURL)
-            {
-                let lines = branchFileContent.components(separatedBy: .newlines)
-                for line in lines {
-                    let components = line.components(separatedBy: "=")
-                    if components.count == 2 {
-                        let key = components[0].trimmingCharacters(in: .whitespaces)
-                        let value = components[1].trimmingCharacters(in: .whitespaces)
-
-                        if key == "BRANCH" {
-                            branch = value
-                            break
-                        }
-                    }
-                }
-            } else {
-                branch = "Unknown"
-            }
+            var branch = BuildDetails.default.branchAndSha
 
 
             let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
             let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
             let pump_ = pumpManager?.localizedTitle ?? ""
             let pump_ = pumpManager?.localizedTitle ?? ""
@@ -919,7 +972,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 scheduled_basal: 0,
                 scheduled_basal: 0,
                 total_average: 0
                 total_average: 0
             )
             )
-            let processedGlucoseStats = await glucoseStats
+            guard let processedGlucoseStats = await glucoseStats else { return }
             let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
             let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
 
 
             let dailystat = await Statistics(
             let dailystat = await Statistics(
@@ -930,7 +983,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 Build_Number: build ?? "1",
                 Build_Number: build ?? "1",
                 Branch: branch,
                 Branch: branch,
                 CopyRightNotice: String(copyrightNotice_.prefix(32)),
                 CopyRightNotice: String(copyrightNotice_.prefix(32)),
-                Build_Date: Date(), // TODO: fix this
+                Build_Date: buildDate ?? Date(),
                 Algorithm: algo_,
                 Algorithm: algo_,
                 AdjustmentFactor: af,
                 AdjustmentFactor: af,
                 Pump: pump_,
                 Pump: pump_,
@@ -938,7 +991,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 insulinType: insulin_type.rawValue,
                 insulinType: insulin_type.rawValue,
                 peakActivityTime: iPa,
                 peakActivityTime: iPa,
                 Carbs_24h: await carbTotal,
                 Carbs_24h: await carbTotal,
-                GlucoseStorage_Days: Decimal(roundDouble(processedGlucoseStats.numberofDays, 1)),
+                GlucoseStorage_Days: Decimal(roundDouble(Double(rawValue: processedGlucoseStats.numberofDays) ?? 0.0, 1)),
                 Statistics: Stats(
                 Statistics: Stats(
                     Distribution: processedGlucoseStats.TimeInRange,
                     Distribution: processedGlucoseStats.TimeInRange,
                     Glucose: processedGlucoseStats.avg,
                     Glucose: processedGlucoseStats.avg,
@@ -1098,175 +1151,153 @@ final class BaseAPSManager: APSManager, Injectable {
         return (currentTDD, tddTotalAverage)
         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) {
     private func loopStats(loopStatRecord: LoopStats) {

+ 1 - 1
Trio/Sources/APS/CGM/AppGroupSource.swift

@@ -115,7 +115,7 @@ struct AppGroupSource: GlucoseSource {
     }
     }
 
 
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Group ID: \(Bundle.main.appGroupSuiteName ?? "Not set"))"]
+        [GlucoseSourceKey.description.rawValue: "Group ID: \(Bundle.main.appGroupSuiteName ?? String(localized: "Not set"))"]
     }
     }
 }
 }
 
 

+ 8 - 7
Trio/Sources/APS/CGM/CGMType.swift

@@ -18,7 +18,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
         case .xdrip:
         case .xdrip:
             return "xDrip4iOS"
             return "xDrip4iOS"
         case .simulator:
         case .simulator:
-            return NSLocalizedString("Glucose Simulator", comment: "Glucose Simulator CGM type")
+            return String(localized: "Glucose Simulator", comment: "Glucose Simulator CGM type")
         case .enlite:
         case .enlite:
             return "Medtronic Enlite"
             return "Medtronic Enlite"
         case .plugin:
         case .plugin:
@@ -52,20 +52,21 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     var subtitle: String {
     var subtitle: String {
         switch self {
         switch self {
         case .none:
         case .none:
-            return NSLocalizedString("None", comment: "No CGM selected")
+            return String(localized: "None", comment: "No CGM selected")
         case .nightscout:
         case .nightscout:
-            return NSLocalizedString("Uses your Nightscout as CGM", comment: "Online or internal server")
+            return String(localized: "Uses your Nightscout as CGM", comment: "Online or internal server")
         case .xdrip:
         case .xdrip:
-            return NSLocalizedString(
+            return String(
+                localized:
                 "Using shared app group with external CGM app xDrip4iOS",
                 "Using shared app group with external CGM app xDrip4iOS",
                 comment: "Shared app group xDrip4iOS"
                 comment: "Shared app group xDrip4iOS"
             )
             )
         case .simulator:
         case .simulator:
-            return NSLocalizedString("Glucose Simulator for Demo Only", comment: "Simple simulator")
+            return String(localized: "Glucose Simulator for Demo Only", comment: "Simple simulator")
         case .enlite:
         case .enlite:
-            return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
+            return String(localized: "Minilink transmitter", comment: "Minilink transmitter")
         case .plugin:
         case .plugin:
-            return NSLocalizedString("Plugin CGM", comment: "Plugin CGM")
+            return String(localized: "Plugin CGM", comment: "Plugin CGM")
         }
         }
     }
     }
 }
 }

+ 185 - 90
Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -10,15 +10,7 @@
 ///
 ///
 /// class GlucoseSimulatorSource - main class
 /// class GlucoseSimulatorSource - main class
 /// protocol BloodGlucoseGenerator
 /// protocol BloodGlucoseGenerator
-///  - IntelligentGenerator: BloodGlucoseGenerator
-
-// TODO: Every itteration trend make two steps, but must only one
-
-// TODO: Trend's value sticks to max and min Glucose value (in Glucose Generator)
-
-// TODO: Add reaction to insulin
-
-// TODO: Add probability to set trend's target value. Middle values must have more probability, than max and min.
+///  - OscillatingGenerator: BloodGlucoseGenerator - Generates sinusoidal glucose values around a center point
 
 
 import Combine
 import Combine
 import Foundation
 import Foundation
@@ -26,22 +18,29 @@ import LoopKitUI
 
 
 // MARK: - Glucose simulator
 // MARK: - Glucose simulator
 
 
+/// A class that simulates glucose values for testing purposes.
+/// This class implements the GlucoseSource protocol and provides simulated glucose readings
+/// using different generator strategies.
 final class GlucoseSimulatorSource: GlucoseSource {
 final class GlucoseSimulatorSource: GlucoseSource {
     var cgmManager: CGMManagerUI?
     var cgmManager: CGMManagerUI?
     var glucoseManager: FetchGlucoseManager?
     var glucoseManager: FetchGlucoseManager?
 
 
     private enum Config {
     private enum Config {
-        // min time period to publish data
+        /// Minimum time period between data publications (in seconds)
         static let workInterval: TimeInterval = 300
         static let workInterval: TimeInterval = 300
-        // default BloodGlucose item at first run
-        // 288 = 1 day * 24 hours * 60 minites * 60 seconds / workInterval
+        /// Default number of blood glucose items to generate at first run
+        /// 288 = 1 day * 24 hours * 60 minutes * 60 seconds / workInterval
         static let defaultBGItems = 288
         static let defaultBGItems = 288
     }
     }
 
 
+    /// The last glucose value that was generated
     @Persisted(key: "GlucoseSimulatorLastGlucose") private var lastGlucose = 100
     @Persisted(key: "GlucoseSimulatorLastGlucose") private var lastGlucose = 100
 
 
+    /// The date of the last fetch operation
     @Persisted(key: "GlucoseSimulatorLastFetchDate") private var lastFetchDate: Date! = nil
     @Persisted(key: "GlucoseSimulatorLastFetchDate") private var lastFetchDate: Date! = nil
 
 
+    /// Initializes the glucose simulator source
+    /// Sets up the initial fetch date if not already set
     init() {
     init() {
         if lastFetchDate == nil {
         if lastFetchDate == nil {
             var lastDate = Date()
             var lastDate = Date()
@@ -52,12 +51,13 @@ final class GlucoseSimulatorSource: GlucoseSource {
         }
         }
     }
     }
 
 
+    /// The glucose generator used to create simulated values
+    /// Uses OscillatingGenerator to create a sinusoidal pattern around 120 mg/dL
     private lazy var generator: BloodGlucoseGenerator = {
     private lazy var generator: BloodGlucoseGenerator = {
-        IntelligentGenerator(
-            currentGlucose: lastGlucose
-        )
+        OscillatingGenerator()
     }()
     }()
 
 
+    /// Determines if new glucose values can be generated based on the time elapsed since the last fetch
     private var canGenerateNewValues: Bool {
     private var canGenerateNewValues: Bool {
         guard let lastDate = lastFetchDate else { return true }
         guard let lastDate = lastFetchDate else { return true }
         if Calendar.current.dateComponents([.second], from: lastDate, to: Date()).second! >= Int(Config.workInterval) {
         if Calendar.current.dateComponents([.second], from: lastDate, to: Date()).second! >= Int(Config.workInterval) {
@@ -67,6 +67,9 @@ final class GlucoseSimulatorSource: GlucoseSource {
         }
         }
     }
     }
 
 
+    /// Fetches new glucose values if enough time has passed since the last fetch
+    /// - Parameter timer: Optional dispatch timer (not used in this implementation)
+    /// - Returns: A publisher that emits an array of BloodGlucose objects
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         guard canGenerateNewValues else {
         guard canGenerateNewValues else {
             return Just([]).eraseToAnyPublisher()
             return Just([]).eraseToAnyPublisher()
@@ -86,6 +89,8 @@ final class GlucoseSimulatorSource: GlucoseSource {
         return Just(glucoses).eraseToAnyPublisher()
         return Just(glucoses).eraseToAnyPublisher()
     }
     }
 
 
+    /// Fetches new glucose values if needed
+    /// - Returns: A publisher that emits an array of BloodGlucose objects
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
         fetch(nil)
         fetch(nil)
     }
     }
@@ -93,105 +98,195 @@ final class GlucoseSimulatorSource: GlucoseSource {
 
 
 // MARK: - Glucose generator
 // MARK: - Glucose generator
 
 
+/// Protocol defining the interface for glucose generators
+/// Implementations of this protocol provide different strategies for generating glucose values
 protocol BloodGlucoseGenerator {
 protocol BloodGlucoseGenerator {
+    /// Generates blood glucose values between the specified dates at the given interval
+    /// - Parameters:
+    ///   - startDate: The start date for generating values
+    ///   - finishDate: The end date for generating values
+    ///   - interval: The time interval between generated values
+    /// - Returns: An array of BloodGlucose objects
     func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval: TimeInterval) -> [BloodGlucose]
     func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval: TimeInterval) -> [BloodGlucose]
 }
 }
 
 
-class IntelligentGenerator: BloodGlucoseGenerator {
-    private enum Config {
-        // max and min glucose of trend's target
-        static let maxGlucose = 320
-        static let minGlucose = 45
+/// A glucose generator that creates a sinusoidal pattern around a center value
+/// This generator simulates a realistic oscillating glucose pattern with configurable parameters
+class OscillatingGenerator: BloodGlucoseGenerator {
+    /// Default values for simulator parameters
+    enum Defaults {
+        static let centerValue: Double = 120.0
+        static let amplitude: Double = 45.0
+        static let period: Double = 10800.0 // 3 hours in seconds
+        static let noiseAmplitude: Double = 5.0
+        static let produceStaleValues: Bool = false
     }
     }
 
 
-    // target glucose of trend
-    @Persisted(key: "GlucoseSimulatorTargetValue") private var trendTargetValue = 100
-    // how many steps left in current trend
-    @Persisted(key: "GlucoseSimulatorTargetSteps") private var trendStepsLeft = 1
-    // direction of last step
-    @Persisted(key: "GlucoseSimulatorDirection") private var trandsStepDirection = BloodGlucose.Direction.flat.rawValue
-    var currentGlucose: Int
-    let startup = Date()
-    init(currentGlucose: Int) {
-        self.currentGlucose = currentGlucose
+    /// UserDefaults keys for storing simulator parameters
+    private enum UserDefaultsKeys {
+        static let centerValue = "GlucoseSimulator_CenterValue"
+        static let amplitude = "GlucoseSimulator_Amplitude"
+        static let period = "GlucoseSimulator_Period"
+        static let noiseAmplitude = "GlucoseSimulator_NoiseAmplitude"
+        static let produceStaleValues = "GlucoseSimulator_ProduceStaleValues"
     }
     }
 
 
-    func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval interval: TimeInterval) -> [BloodGlucose] {
-        var result = [BloodGlucose]()
-
-        var _currentDate = startDate
-        while _currentDate <= finishDate {
-            result.append(getNextBloodGlucose(forDate: _currentDate))
-            _currentDate = _currentDate.addingTimeInterval(interval)
-        }
+    /// Amplitude of the oscillation (±45 mg/dL to create range from ~80 to ~170)
+    private var amplitude: Double {
+        get { UserDefaults.standard.double(forKey: UserDefaultsKeys.amplitude) != 0 ?
+            UserDefaults.standard.double(forKey: UserDefaultsKeys.amplitude) :
+            Defaults.amplitude }
+        set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.amplitude) }
+    }
 
 
-        return result
+    /// Period of the oscillation in seconds (3 hours = 10800 seconds)
+    private var period: Double {
+        get { UserDefaults.standard.double(forKey: UserDefaultsKeys.period) != 0 ?
+            UserDefaults.standard.double(forKey: UserDefaultsKeys.period) :
+            Defaults.period }
+        set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.period) }
     }
     }
 
 
-    // get next glucose's value in current trend
-    private func getNextBloodGlucose(forDate date: Date) -> BloodGlucose {
-        let previousGlucose = currentGlucose
-        makeStepInTrend()
-        trandsStepDirection = getDirection(fromGlucose: previousGlucose, toGlucose: currentGlucose).rawValue
-        let glucose = BloodGlucose(
-            _id: UUID().uuidString,
-            sgv: currentGlucose,
-            direction: BloodGlucose.Direction(rawValue: trandsStepDirection),
-            date: Decimal(Int(date.timeIntervalSince1970) * 1000),
-            dateString: date,
-            unfiltered: Decimal(currentGlucose),
-            filtered: nil,
-            noise: nil,
-            glucose: currentGlucose,
-            type: nil,
-            activationDate: startup,
-            sessionStartDate: startup,
-            transmitterID: "SIMULATOR"
-        )
-        return glucose
+    /// Center value of the oscillation (target glucose level)
+    private var centerValue: Double {
+        get { UserDefaults.standard.double(forKey: UserDefaultsKeys.centerValue) != 0 ?
+            UserDefaults.standard.double(forKey: UserDefaultsKeys.centerValue) :
+            Defaults.centerValue }
+        set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.centerValue) }
     }
     }
 
 
-    private func setNewRandomTarget() {
-        guard trendTargetValue > 0 else {
-            trendTargetValue = Array(80 ... 110).randomElement()!
-            return
-        }
-        let difference = (Array(-50 ... -20) + Array(20 ... 50)).randomElement()!
-        let _value = trendTargetValue + difference
-        if _value <= Config.minGlucose {
-            trendTargetValue = Config.minGlucose
-        } else if _value >= Config.maxGlucose {
-            trendTargetValue = Config.maxGlucose
-        } else {
-            trendTargetValue = _value
-        }
+    /// Amplitude of random noise to add to the values (±5 mg/dL)
+    private var noiseAmplitude: Double {
+        get { UserDefaults.standard.double(forKey: UserDefaultsKeys.noiseAmplitude) != 0 ?
+            UserDefaults.standard.double(forKey: UserDefaultsKeys.noiseAmplitude) :
+            Defaults.noiseAmplitude }
+        set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.noiseAmplitude) }
     }
     }
 
 
-    private func setNewRandomSteps() {
-        trendStepsLeft = Array(3 ... 8).randomElement()!
+    /// Whether to produce stale (unchanging) glucose values
+    var produceStaleValues: Bool {
+        get { UserDefaults.standard.bool(forKey: UserDefaultsKeys.produceStaleValues) }
+        set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.produceStaleValues) }
     }
     }
 
 
-    private func getDirection(fromGlucose from: Int, toGlucose to: Int) -> BloodGlucose.Direction {
-        BloodGlucose.Direction(trend: Int(to - from))
+    /// Start date for the simulation
+    private let startup = Date()
+
+    /// Last generated glucose value for stale mode
+    private var lastGeneratedGlucose: Int?
+
+    /// Provides information string to describe the simulator as glucose source
+    func sourceInfo() -> [String: Any]? {
+        [GlucoseSourceKey.description.rawValue: "Glucose simulator"]
     }
     }
 
 
-    private func generateNewTrend() {
-        setNewRandomTarget()
-        setNewRandomSteps()
+    /// Reset all parameters to default values
+    func resetToDefaults() {
+        centerValue = Defaults.centerValue
+        amplitude = Defaults.amplitude
+        period = Defaults.period
+        noiseAmplitude = Defaults.noiseAmplitude
+        produceStaleValues = Defaults.produceStaleValues
+        lastGeneratedGlucose = nil
     }
     }
 
 
-    private func makeStepInTrend() {
-        guard trendStepsLeft > 0 else { return }
+    /// Generates blood glucose values between the specified dates at the given interval
+    /// - Parameters:
+    ///   - startDate: The start date for generating values
+    ///   - finishDate: The end date for generating values
+    ///   - interval: The time interval between generated values
+    /// - Returns: An array of BloodGlucose objects with sinusoidal pattern
+    func getBloodGlucoses(startDate: Date, finishDate: Date, withInterval interval: TimeInterval) -> [BloodGlucose] {
+        var result = [BloodGlucose]()
+        var currentDate = startDate
+
+        while currentDate <= finishDate {
+            let glucose: Int
+            let direction: BloodGlucose.Direction
+
+            if produceStaleValues, lastGeneratedGlucose != nil {
+                // In stale mode, use the last generated glucose value
+                glucose = lastGeneratedGlucose!
+                direction = .flat
+            } else {
+                // Generate a new glucose value
+                glucose = generate(date: currentDate)
+                direction = calculateDirection(at: currentDate)
+                lastGeneratedGlucose = glucose
+            }
 
 
-        currentGlucose +=
-            Int(Double((trendTargetValue - currentGlucose) / trendStepsLeft) * [0.3, 0.6, 1, 1.3, 1.6, 2.0].randomElement()!)
-        trendStepsLeft -= 1
-        if trendStepsLeft == 0 {
-            generateNewTrend()
+            // Create BloodGlucose with the correct constructor
+            let bloodGlucose = BloodGlucose(
+                _id: UUID().uuidString,
+                sgv: glucose,
+                direction: direction,
+                date: Decimal(Int(currentDate.timeIntervalSince1970) * 1000),
+                dateString: currentDate,
+                unfiltered: Decimal(glucose),
+                filtered: nil,
+                noise: nil,
+                glucose: glucose,
+                type: nil,
+                activationDate: startup,
+                sessionStartDate: startup,
+                transmitterID: "SIMULATOR"
+            )
+
+            result.append(bloodGlucose)
+            currentDate = currentDate.addingTimeInterval(interval)
         }
         }
+
+        return result
     }
     }
 
 
-    func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Glucose simulator"]
+    /// Generates a glucose value for the specified date using a sinusoidal function
+    /// - Parameter date: The date for which to generate the glucose value
+    /// - Returns: An integer representing the glucose value in mg/dL
+    private func generate(date: Date) -> Int {
+        // Time in seconds since 1970
+        let timeSeconds = date.timeIntervalSince1970
+
+        // Calculate sine value
+        let sinValue = sin(2.0 * .pi * timeSeconds / period)
+
+        // Random noise
+        let noise = Double.random(in: -noiseAmplitude ... noiseAmplitude)
+
+        // Calculate glucose value: center + amplitude * sine + noise
+        let glucoseValue = centerValue + amplitude * sinValue + noise
+
+        // Return as integer
+        return Int(glucoseValue)
+    }
+
+    /// Calculates the direction (trend) of glucose change at the specified date
+    /// - Parameter date: The date for which to calculate the direction
+    /// - Returns: A BloodGlucose.Direction value indicating the trend
+    private func calculateDirection(at date: Date) -> BloodGlucose.Direction {
+        // Time in seconds since 1970
+        let timeSeconds = date.timeIntervalSince1970
+
+        // Calculate derivative of sine function (cosine)
+        let cosValue = cos(2.0 * .pi * timeSeconds / period)
+
+        // Slope of the curve at this point
+        let slope = -amplitude * 2.0 * .pi / period * cosValue
+
+        // Determine direction based on slope
+        if abs(slope) < 0.2 {
+            return .flat
+        } else if slope > 0 {
+            if slope > 1.0 {
+                return .singleUp
+            } else {
+                return .fortyFiveUp
+            }
+        } else {
+            if slope < -1.0 {
+                return .singleDown
+            } else {
+                return .fortyFiveDown
+            }
+        }
     }
     }
 }
 }

+ 2 - 2
Trio/Sources/APS/CGM/PluginSource.swift

@@ -159,9 +159,9 @@ extension PluginSource: CGMManagerDelegate {
         UUID().uuidString
         UUID().uuidString
     }
     }
 
 
-    func cgmManager(_ cgmManager: CGMManager, didUpdate status: CGMManagerStatus) {
+    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
         debug(.deviceManager, "CGM Manager did update state to \(status)")
         debug(.deviceManager, "CGM Manager did update state to \(status)")
-        
+
         processQueue.async {
         processQueue.async {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
                 self.cgmHasValidSensorSession = status.hasValidSensorSession

+ 15 - 9
Trio/Sources/APS/DeviceDataManager.swift

@@ -50,7 +50,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
 
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
-    private let processQueue = DispatchQueue.markedQueue(label: "BaseDeviceDataManager.processQueue")
+    private let processQueue = DispatchQueue.markedQueue(label: "BaseDeviceDataManager.processQueue", qos: .userInitiated)
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() var alertHistoryStorage: AlertHistoryStorage!
     @Injected() var alertHistoryStorage: AlertHistoryStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
@@ -512,15 +512,21 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     ) {
     ) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         dispatchPrecondition(condition: .onQueue(processQueue))
 
 
-        // filter buggy TBRs > maxBasal from MDT
-        let events = events.filter {
-            // type is optional...
-            guard let type = $0.type, type == .tempBasal else { return true }
-            return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal)
+        Task {
+            do {
+                // filter buggy TBRs > maxBasal from MDT
+                let events = events.filter {
+                    // type is optional...
+                    guard let type = $0.type, type == .tempBasal else { return true }
+                    return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal)
+                }
+                try await pumpHistoryStorage.storePumpEvents(events)
+                lastEventDate = events.last?.date
+                completion(nil)
+            } catch {
+                debug(.deviceManager, "\(DebuggingIdentifiers.failed) Failed to store pump events: \(error)")
+            }
         }
         }
-        pumpHistoryStorage.storePumpEvents(events)
-        lastEventDate = events.last?.date
-        completion(nil)
     }
     }
 
 
     func pumpManager(
     func pumpManager(

+ 59 - 85
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -8,8 +8,6 @@ import Swinject
 import UIKit
 import UIKit
 
 
 protocol FetchGlucoseManager: SourceInfoProvider {
 protocol FetchGlucoseManager: SourceInfoProvider {
-    func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
-    func refreshCGM()
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource()
     func deleteGlucoseSource()
     func removeCalibrations()
     func removeCalibrations()
@@ -77,6 +75,44 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         subscribe()
         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!
     var glucoseSource: GlucoseSource!
 
 
     func removeCalibrations() {
     func removeCalibrations() {
@@ -171,32 +207,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return Manager.init(rawState: rawState)
         return Manager.init(rawState: rawState)
     }
     }
 
 
-    /// function called when a callback is fired by CGM BLE - no more used
-    public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
-        let syncDate = glucoseStorage.syncDate()
-        debug(.deviceManager, "CGM BLE FETCHGLUCOSE  : SyncDate is \(syncDate)")
-        glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
-    }
-
-    /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
-    public func refreshCGM() {
-        debug(.deviceManager, "refreshCGM by pump")
-
-        Publishers.CombineLatest(
-            Just(glucoseStorage.syncDate()),
-            glucoseSource.fetchIfNeeded()
-        )
-        .eraseToAnyPublisher()
-        .receive(on: processQueue)
-        .sink { syncDate, glucose in
-            debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
-            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose)
-        }
-        .store(in: &lifetime)
-    }
-
-    private func fetchGlucose() -> [GlucoseStored]? {
-        CoreDataStack.shared.fetchEntities(
+    private func fetchGlucose() async throws -> [GlucoseStored]? {
+        try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgo,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -206,9 +218,13 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         ) as? [GlucoseStored]
         ) as? [GlucoseStored]
     }
     }
 
 
-    private func processGlucose() -> [BloodGlucose] {
-        context.performAndWait {
-            guard let results = fetchGlucose() else { return [] }
+    private func processGlucose() async throws -> [BloodGlucose] {
+        let results = try await fetchGlucose()
+
+        return try await context.perform {
+            guard let results else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return results.map { result in
             return results.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     sgv: Int(result.glucose),
                     sgv: Int(result.glucose),
@@ -225,7 +241,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
         }
     }
     }
 
 
-    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) {
+    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
         // calibration add if required only for sensor
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
         let newGlucose = overcalibrate(entries: glucose)
 
 
@@ -234,37 +250,33 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
 
         // start background time extension
         // start background time extension
         var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
         var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
-        backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
+        backGroundFetchBGTaskID = await UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
             guard let bg = backGroundFetchBGTaskID else { return }
             guard let bg = backGroundFetchBGTaskID else { return }
             UIApplication.shared.endBackgroundTask(bg)
             UIApplication.shared.endBackgroundTask(bg)
             backGroundFetchBGTaskID = .invalid
             backGroundFetchBGTaskID = .invalid
         }
         }
 
 
-        guard newGlucose.isNotEmpty else {
+        defer {
             if let backgroundTask = backGroundFetchBGTaskID {
             if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
+                Task {
+                    await UIApplication.shared.endBackgroundTask(backgroundTask)
+                }
                 backGroundFetchBGTaskID = .invalid
                 backGroundFetchBGTaskID = .invalid
             }
             }
-            return
         }
         }
 
 
+        guard newGlucose.isNotEmpty else { return }
+
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
 
-        guard filtered.isNotEmpty else {
-            // end of the Background tasks
-            if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundFetchBGTaskID = .invalid
-            }
-            return
-        }
+        guard filtered.isNotEmpty else { return }
         debug(.deviceManager, "New glucose found")
         debug(.deviceManager, "New glucose found")
 
 
         // filter the data if it is the case
         // filter the data if it is the case
         if settingsManager.settings.smoothGlucose {
         if settingsManager.settings.smoothGlucose {
             // limited to 30 min of old glucose data
             // limited to 30 min of old glucose data
-            let oldGlucoseValues = processGlucose()
+            let oldGlucoseValues = try await processGlucose()
 
 
             var smoothedValues = oldGlucoseValues + filtered
             var smoothedValues = oldGlucoseValues + filtered
             // smooth with 3 repeats
             // smooth with 3 repeats
@@ -275,47 +287,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             filtered = smoothedValues.filter { $0.dateString > syncDate }
             filtered = smoothedValues.filter { $0.dateString > syncDate }
         }
         }
 
 
-        glucoseStorage.storeGlucose(filtered)
-
+        try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
         deviceDataManager.heartbeat(date: Date())
-
-        // End of the Background tasks
-        if let backgroundTask = backGroundFetchBGTaskID {
-            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
-                    self.glucoseStoreAndHeartDecision(
-                        syncDate: syncDate,
-                        glucose: newGlucose
-                    )
-                }
-                .store(in: &self.lifetime)
-            }
-            .store(in: &lifetime)
-        timer.fire()
-        timer.resume()
     }
     }
 
 
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {
@@ -333,6 +306,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let overcalibration = overcalibration {
         if let overcalibration = overcalibration {
             return entries.map { entry in
             return entries.map { entry in
                 var entry = entry
                 var entry = entry
+                guard entry.glucose != nil else { return entry }
                 entry.glucose = Int(overcalibration(entry.glucose!))
                 entry.glucose = Int(overcalibration(entry.glucose!))
                 entry.sgv = Int(overcalibration(entry.sgv!))
                 entry.sgv = Int(overcalibration(entry.sgv!))
                 return entry
                 return entry

+ 39 - 35
Trio/Sources/APS/FetchTreatmentsManager.swift

@@ -30,48 +30,52 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
                 debug(.nightscout, "Start fetching carbs and temptargets")
                 debug(.nightscout, "Start fetching carbs and temptargets")
 
 
                 Task {
                 Task {
-                    // Fetch carbs and temp targets concurrently
-                    async let carbs = self.nightscoutManager.fetchCarbs()
-                    async let tempTargets = self.nightscoutManager.fetchTempTargets()
+                    do {
+                        // Fetch carbs and temp targets concurrently
+                        async let carbs = self.nightscoutManager.fetchCarbs()
+                        async let tempTargets = self.nightscoutManager.fetchTempTargets()
 
 
-                    // 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)
-                    }
+                        // Filter and store if not from "Trio"
+                        let filteredCarbs = await carbs.filter { $0.enteredBy != CarbsEntry.local }
+                        if filteredCarbs.isNotEmpty {
+                            try await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
+                        }
 
 
-                    // Filter and store if not from Trio
-                    let filteredTargets = await tempTargets.filter { $0.enteredBy != TempTarget.local }
-                    if filteredTargets.isNotEmpty {
-                        // Sort temp targets by creation date
-                        let sortedTargets = filteredTargets.sorted { $0.createdAt < $1.createdAt }
+                        // Filter and store if not from Trio
+                        let filteredTargets = await tempTargets.filter { $0.enteredBy != TempTarget.local }
+                        if filteredTargets.isNotEmpty {
+                            // Sort temp targets by creation date
+                            let sortedTargets = filteredTargets.sorted { $0.createdAt < $1.createdAt }
 
 
-                        // Iterate and store each temp target
-                        for (index, tempTarget) in sortedTargets.enumerated() {
-                            // Skip saving if a Temp Target with the same date already exists or it's a cancel target
-                            guard await !self.tempTargetsStorage.existsTempTarget(with: tempTarget.createdAt),
-                                  tempTarget.reason != TempTarget.cancel
-                            else {
-                                debug(
-                                    .nightscout,
-                                    "Skipping temp target with date: \(tempTarget.date ?? Date.distantPast)"
-                                )
-                                continue
-                            }
+                            // Iterate and store each temp target
+                            for (index, tempTarget) in sortedTargets.enumerated() {
+                                // Skip saving if a Temp Target with the same date already exists or it's a cancel target
+                                guard await !self.tempTargetsStorage.existsTempTarget(with: tempTarget.createdAt),
+                                      tempTarget.reason != TempTarget.cancel
+                                else {
+                                    debug(
+                                        .nightscout,
+                                        "Skipping temp target with date: \(tempTarget.date ?? Date.distantPast)"
+                                    )
+                                    continue
+                                }
 
 
-                            // Create a mutable copy and set enabled for the last temp target
-                            var mutableTempTarget = tempTarget
-                            mutableTempTarget.enabled = (index == sortedTargets.count - 1)
+                                // Create a mutable copy and set enabled for the last temp target
+                                var mutableTempTarget = tempTarget
+                                mutableTempTarget.enabled = (index == sortedTargets.count - 1)
 
 
-                            // Save to Core Data
-                            await self.tempTargetsStorage.storeTempTarget(tempTarget: mutableTempTarget)
-                        }
+                                // Save to Core Data
+                                try await self.tempTargetsStorage.storeTempTarget(tempTarget: mutableTempTarget)
+                            }
 
 
-                        // Save the temp targets to JSON so that they get used by oref
-                        self.tempTargetsStorage.saveTempTargetsToStorage(sortedTargets)
+                            // Save the temp targets to JSON so that they get used by oref
+                            self.tempTargetsStorage.saveTempTargetsToStorage(sortedTargets)
 
 
-                        // Update Adjustments View
-                        Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
+                            // Update Adjustments View
+                            Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
+                        }
+                    } catch {
+                        debug(.default, "\(DebuggingIdentifiers.failed) error in \(#file) \(#function): \(error)")
                     }
                     }
                 }
                 }
             }
             }

+ 41 - 35
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
     // 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,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
@@ -111,9 +111,9 @@ final class OpenAPS {
             batchSize: 24
             batchSize: 24
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return ""
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             // convert to JSON
             // convert to JSON
@@ -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,
             ofType: CarbEntryStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -130,9 +130,9 @@ final class OpenAPS {
             ascending: false
             ascending: false
         )
         )
 
 
-        let json = await context.perform {
+        let json = try await context.perform {
             guard let carbResults = results as? [CarbEntryStored] else {
             guard let carbResults = results as? [CarbEntryStored] else {
-                return ""
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
@@ -170,8 +170,8 @@ final class OpenAPS {
         return json
         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,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpHistoryLast1440Minutes,
             predicate: NSPredicate.pumpHistoryLast1440Minutes,
@@ -180,9 +180,9 @@ final class OpenAPS {
             batchSize: 50
             batchSize: 50
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let pumpEventResults = results as? [PumpEventStored] else {
             guard let pumpEventResults = results as? [PumpEventStored] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return pumpEventResults.map(\.objectID)
             return pumpEventResults.map(\.objectID)
@@ -302,10 +302,10 @@ final class OpenAPS {
             reservoir,
             reservoir,
             preferences
             preferences
         ) = await (
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
-            carbs,
-            glucose,
-            oref2,
+            try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
+            try carbs,
+            try glucose,
+            try oref2,
             profileAsync,
             profileAsync,
             basalAsync,
             basalAsync,
             autosenseAsync,
             autosenseAsync,
@@ -366,12 +366,12 @@ final class OpenAPS {
 
 
             return determination
             return determination
         } else {
         } else {
-            return nil
+            throw APSError.apsError(message: "Determination is nil")
         }
         }
     }
     }
 
 
-    func oref2() async -> RawJSON {
-        await context.perform {
+    func oref2() async throws -> RawJSON {
+        try await context.perform {
             // Retrieve user preferences
             // Retrieve user preferences
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             let weightPercentage = userPreferences?.weightPercentage ?? 1.0
             let weightPercentage = userPreferences?.weightPercentage ?? 1.0
@@ -381,10 +381,10 @@ final class OpenAPS {
             // Fetch historical events for Total Daily Dose (TDD) calculation
             // Fetch historical events for Total Daily Dose (TDD) calculation
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.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
             // Fetch the last active Override
-            let activeOverrides = self.fetchActiveOverrides()
+            let activeOverrides = try self.fetchActiveOverrides()
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
@@ -448,9 +448,9 @@ final class OpenAPS {
 
 
         // Await the results of asynchronous tasks
         // Await the results of asynchronous tasks
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs),
-            carbs,
-            glucose,
+            try parsePumpHistory(await pumpHistoryObjectIDs),
+            try carbs,
+            try glucose,
             getProfile,
             getProfile,
             getBasalProfile,
             getBasalProfile,
             getTempTargets
             getTempTargets
@@ -477,7 +477,7 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    func createProfiles() async {
+    func createProfiles() async throws {
         debug(.openAPS, "Start creating pump profile and user profile")
         debug(.openAPS, "Start creating pump profile and user profile")
 
 
         // Load required settings and profiles asynchronously
         // Load required settings and profiles asynchronously
@@ -507,9 +507,9 @@ final class OpenAPS {
         var adjustedPreferences = preferences
         var adjustedPreferences = preferences
 
 
         // Check for active Temp Targets and adjust HBT if necessary
         // 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
             // 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,
                activeTempTarget.enabled,
                let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
                let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
                activeHBT != defaultHalfBasalTarget
                activeHBT != defaultHalfBasalTarget
@@ -555,11 +555,12 @@ final class OpenAPS {
                 .apsManager,
                 .apsManager,
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create pump profile and normal profile: \(error)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create pump profile and normal profile: \(error)"
             )
             )
+            throw error
         }
         }
     }
     }
 
 
     private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
     private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
-        await withCheckedContinuation { continuation in
+        try await withCheckedThrowingContinuation { continuation in
             jsWorker.inCommonContext { worker in
             jsWorker.inCommonContext { worker in
                 worker.evaluateBatch(scripts: [
                 worker.evaluateBatch(scripts: [
                     Script(name: Prepare.log),
                     Script(name: Prepare.log),
@@ -753,7 +754,12 @@ final class OpenAPS {
         }
         }
 
 
         if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
         if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
-            return Script(name: name, body: try! String(contentsOf: url))
+            do {
+                let body = try String(contentsOf: url)
+                return Script(name: name, body: body)
+            } catch {
+                debug(.openAPS, "Failed to load script \(name): \(error)")
+            }
         }
         }
 
 
         return nil
         return nil
@@ -801,8 +807,8 @@ final class OpenAPS {
 
 
 // Non-Async fetch methods for oref2
 // Non-Async fetch methods for oref2
 extension OpenAPS {
 extension OpenAPS {
-    func fetchActiveTempTargets() -> [TempTargetStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveTempTargets() throws -> [TempTargetStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.lastActiveTempTarget,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -812,8 +818,8 @@ extension OpenAPS {
         ) as? [TempTargetStored] ?? []
         ) as? [TempTargetStored] ?? []
     }
     }
 
 
-    func fetchActiveOverrides() -> [OverrideStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveOverrides() throws -> [OverrideStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.lastActiveOverride,
             predicate: NSPredicate.lastActiveOverride,
@@ -823,8 +829,8 @@ extension OpenAPS {
         ) as? [OverrideStored] ?? []
         ) 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,
             ofType: OrefDetermination.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),
             predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),

+ 31 - 29
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -10,14 +10,14 @@ protocol CarbsObserver {
 
 
 protocol CarbsStorage {
 protocol CarbsStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     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 deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     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 {
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -38,11 +38,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
 
 
-    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
+    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async throws {
         var entriesToStore = entries
         var entriesToStore = entries
 
 
         if areFetchedFromRemote {
         if areFetchedFromRemote {
-            entriesToStore = await filterRemoteEntries(entries: entriesToStore)
+            entriesToStore = try await filterRemoteEntries(entries: entriesToStore)
         }
         }
 
 
         // Check for FPU-only entries (fat/protein without carbs)
         // Check for FPU-only entries (fat/protein without carbs)
@@ -71,9 +71,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         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
         // 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,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.predicateForOneDayAgo,
             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,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
@@ -348,9 +348,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await coredataContext.perform {
+        return try await coredataContext.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
             guard let carbEntries = results as? [CarbEntryStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return carbEntries.map { result in
             return carbEntries.map { result in
@@ -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,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
@@ -387,8 +387,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await coredataContext.perform {
-            guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fpuEntries = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fpuEntries.map { result in
             return fpuEntries.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
@@ -415,8 +417,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,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
@@ -424,11 +426,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
+        return try await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return carbEntries.map { result in
             return carbEntries.map { result in
                 CarbsEntry(
                 CarbsEntry(
                     id: result.id?.uuidString,
                     id: result.id?.uuidString,
@@ -446,8 +448,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,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
@@ -455,11 +457,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
+        return try await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return carbEntries.map { result in
             return carbEntries.map { result in
                 CarbsEntry(
                 CarbsEntry(
                     id: result.id?.uuidString,
                     id: result.id?.uuidString,

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

@@ -26,36 +26,43 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
     ///
     ///
     /// - Returns: An array of `ContactImageEntry` objects.
     /// - Returns: An array of `ContactImageEntry` objects.
     func fetchContactImageEntries() async -> [ContactImageEntry] {
     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 try await backgroundContext.perform {
+                guard let fetchedContactImageEntries = results as? [ContactImageEntryStored]
+                else { throw CoreDataError.fetchError(function: #function, file: #file)
+                }
 
 
-            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 ?? String(localized: "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 []
         }
         }
     }
     }
 
 

+ 49 - 15
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -4,7 +4,7 @@ import Foundation
 import Swinject
 import Swinject
 
 
 protocol DeterminationStorage {
 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 getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func fetchForecastObjects(
     func fetchForecastObjects(
@@ -12,29 +12,32 @@ protocol DeterminationStorage {
         in context: NSManagedObjectContext
         in context: NSManagedObjectContext
     ) async -> (UUID, Forecast?, [ForecastValue])
     ) async -> (UUID, Forecast?, [ForecastValue])
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
+    func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
+    async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
 }
 }
 
 
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let context = CoreDataStack.shared.newTaskContext()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         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,
             ofType: OrefDetermination.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: predicate,
             predicate: predicate,
             key: "deliverAt",
             key: "deliverAt",
             ascending: false,
             ascending: false,
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
+        return try await context.perform {
+            guard let fetchedResults = results as? [OrefDetermination] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -77,7 +80,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         }
         }
     }
     }
 
 
-    // Fetch forecast value IDs for a given data set
+    // Fetch forecast objects for a given data set
     func fetchForecastObjects(
     func fetchForecastObjects(
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         in context: NSManagedObjectContext
         in context: NSManagedObjectContext
@@ -112,19 +115,19 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 
 
     // Convert NSSet to array of Ints for Predictions
     // Convert NSSet to array of Ints for Predictions
     func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
     func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
-        let forecastIDs = await getForecastIDs(for: determinationID, in: backgroundContext)
+        let forecastIDs = await getForecastIDs(for: determinationID, in: context)
 
 
         var forecastValuesList: [Int] = []
         var forecastValuesList: [Int] = []
 
 
         for forecastID in forecastIDs {
         for forecastID in forecastIDs {
-            await backgroundContext.perform {
-                if let forecast = try? self.backgroundContext.existingObject(with: forecastID) as? Forecast {
+            await context.perform {
+                if let forecast = try? self.context.existingObject(with: forecastID) as? Forecast {
                     // Filter the forecast based on the type
                     // Filter the forecast based on the type
                     if forecast.type == type {
                     if forecast.type == type {
                         let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
                         let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
 
 
                         for forecastValueID in forecastValueIDs {
                         for forecastValueID in forecastValueIDs {
-                            if let forecastValue = try? self.backgroundContext
+                            if let forecastValue = try? self.context
                                 .existingObject(with: forecastValueID) as? ForecastValue
                                 .existingObject(with: forecastValueID) as? ForecastValue
                             {
                             {
                                 let forecastValueInt = Int(forecastValue.value)
                                 let forecastValueInt = Int(forecastValue.value)
@@ -153,9 +156,9 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             uam: await parseForecastValues(ofType: "uam", from: determinationId)
             uam: await parseForecastValues(ofType: "uam", from: determinationId)
         )
         )
 
 
-        return await backgroundContext.perform {
+        return await context.perform {
             do {
             do {
-                let orefDetermination = try self.backgroundContext.existingObject(with: determinationId) as? OrefDetermination
+                let orefDetermination = try self.context.existingObject(with: determinationId) as? OrefDetermination
 
 
                 // Check if the fetched object is of the expected type
                 // Check if the fetched object is of the expected type
                 if let orefDetermination = orefDetermination {
                 if let orefDetermination = orefDetermination {
@@ -201,4 +204,35 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             return result
             return result
         }
         }
     }
     }
+
+    func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
+    async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
+    {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: Forecast.self,
+            onContext: context,
+            predicate: NSPredicate(format: "orefDetermination = %@", determinationID),
+            key: "type",
+            ascending: true,
+            relationshipKeyPathsForPrefetching: ["forecastValues"]
+        )
+
+        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+        await context.perform {
+            if let forecasts = results as? [Forecast] {
+                for forecast in forecasts {
+                    // Use the helper property that already sorts by index
+                    let sortedValues = forecast.forecastValuesArray
+                    result.append((
+                        id: UUID(),
+                        forecastID: forecast.objectID,
+                        forecastValueIDs: sortedValues.map(\.objectID)
+                    ))
+                }
+            }
+        }
+
+        return result
+    }
 }
 }

+ 227 - 159
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -9,20 +9,20 @@ import Swinject
 
 
 protocol GlucoseStorage {
 protocol GlucoseStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storeGlucose(_ glucose: [BloodGlucose])
+    func storeGlucose(_ glucose: [BloodGlucose]) async throws
     func addManualGlucose(glucose: Int)
     func addManualGlucose(glucose: Int)
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
     func lastGlucoseDate() -> Date
     func isGlucoseFresh() -> Bool
     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 }
     var alarm: GlucoseAlarm? { get }
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
 }
@@ -60,120 +60,163 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return formatter
         return formatter
     }
     }
 
 
-    func storeGlucose(_ glucose: [BloodGlucose]) {
-        processQueue.sync {
-            self.coredataContext.perform {
-                let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
-                let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
-                fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
-                    NSPredicate(format: "date IN %@", datesToCheck),
-                    NSPredicate.predicateForOneDayAgo
-                ])
-                fetchRequest.propertiesToFetch = ["date"]
-                fetchRequest.resultType = .dictionaryResultType
-
-                var existingDates = Set<Date>()
-                do {
-                    let results = try self.coredataContext.fetch(fetchRequest) as? [NSDictionary]
-                    existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
-                } catch {
-                    debugPrint("Failed to fetch existing glucose dates: \(error)")
-                }
+    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 }
 
 
-                var filteredGlucose = glucose.filter { !existingDates.contains($0.dateString) }
-
-                // prepare batch insert
-                let batchInsert = NSBatchInsertRequest(
-                    entity: GlucoseStored.entity(),
-                    managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
-                        guard let glucoseEntry = managedObject as? GlucoseStored, !filteredGlucose.isEmpty else {
-                            return true // Stop if there are no more items
-                        }
-                        let entry = filteredGlucose.removeFirst()
-                        glucoseEntry.id = UUID()
-                        glucoseEntry.glucose = Int16(entry.glucose ?? 0)
-                        glucoseEntry.date = entry.dateString
-                        glucoseEntry.direction = entry.direction?.rawValue
-                        glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
-                        glucoseEntry.isUploadedToHealth = false /// the value is not uploaded to Health (yet)
-                        glucoseEntry.isUploadedToTidepool = false /// the value is not uploaded to Tidepool (yet)
-                        return false // Continue processing
-                    }
+            do {
+                // Store glucose values in Core Data
+                try self.storeGlucoseInCoreData(newGlucose)
+            } catch {
+                throw CoreDataError.creationError(
+                    function: #function,
+                    file: #fileID
                 )
                 )
+            }
+
+            // Store CGM state if needed
+            self.storeCGMState(glucose)
+        }
+    }
+
+    private func filterNewGlucoseValues(_ glucose: [BloodGlucose]) -> [BloodGlucose] {
+        let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
+        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
+        fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+            NSPredicate(format: "date IN %@", datesToCheck),
+            NSPredicate.predicateForOneDayAgo
+        ])
+        fetchRequest.propertiesToFetch = ["date"]
+        fetchRequest.resultType = .dictionaryResultType
+
+        var existingDates = Set<Date>()
+        do {
+            let results = try coredataContext.fetch(fetchRequest) as? [NSDictionary]
+            existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
+        } catch {
+            debugPrint("Failed to fetch existing glucose dates: \(error)")
+        }
+
+        return glucose.filter { !existingDates.contains($0.dateString) }
+    }
+
+    private func storeGlucoseInCoreData(_ glucose: [BloodGlucose]) throws {
+        if glucose.count > 1 {
+            try storeGlucoseBatch(glucose)
+        } else {
+            try storeGlucoseRegular(glucose)
+        }
+    }
+
+    private func storeGlucoseRegular(_ glucose: [BloodGlucose]) throws {
+        for entry in glucose {
+            let glucoseEntry = GlucoseStored(context: coredataContext)
+            configureGlucoseEntry(glucoseEntry, with: entry)
+        }
 
 
-                // process batch insert
-                do {
-                    try self.coredataContext.execute(batchInsert)
-
-                    // Notify subscribers that there is a new glucose value
-                    // We need to do this because the due to the batch insert there is no ManagedObjectContext notification
-                    self.updateSubject.send(())
-                } catch {
-                    debugPrint(
-                        "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
-                    )
+        guard coredataContext.hasChanges else { return }
+        try coredataContext.save()
+    }
+
+    private func storeGlucoseBatch(_ glucose: [BloodGlucose]) throws {
+        var remainingGlucose = glucose
+        let batchInsert = NSBatchInsertRequest(
+            entity: GlucoseStored.entity(),
+            managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
+                guard let glucoseEntry = managedObject as? GlucoseStored,
+                      !remainingGlucose.isEmpty
+                else {
+                    return true
                 }
                 }
+                let entry = remainingGlucose.removeFirst()
+                self.configureGlucoseEntry(glucoseEntry, with: entry)
+                return false
+            }
+        )
+        try coredataContext.execute(batchInsert)
+        // Only send update for batch insert since regular save triggers CoreData notifications
+        updateSubject.send()
+    }
+
+    private func configureGlucoseEntry(_ entry: GlucoseStored, with glucose: BloodGlucose) {
+        entry.id = UUID()
+        entry.glucose = Int16(glucose.glucose ?? 0)
+        entry.date = glucose.dateString
+        entry.direction = glucose.direction?.rawValue
+        entry.isUploadedToNS = false
+        entry.isUploadedToHealth = false
+        entry.isUploadedToTidepool = false
+    }
 
 
-                debug(.deviceManager, "start storage cgmState")
-                self.storage.transaction { storage in
-                    let file = OpenAPS.Monitor.cgmState
-                    var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
-                    var updated = false
-                    for x in glucose {
-                        debug(.deviceManager, "storeGlucose \(x)")
-                        guard let sessionStartDate = x.sessionStartDate else {
-                            continue
-                        }
-                        if let lastTreatment = treatments.last,
-                           let createdAt = lastTreatment.createdAt,
-                           // When a new Dexcom sensor is started, it produces multiple consecutive
-                           // startDates. Disambiguate them by only allowing a session start per minute.
-                           abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
-                        {
-                            continue
-                        }
-                        var notes = ""
-                        if let t = x.transmitterID {
-                            notes = t
-                        }
-                        if let a = x.activationDate {
-                            notes = "\(notes) activated on \(a)"
-                        }
-                        let treatment = NightscoutTreatment(
-                            duration: nil,
-                            rawDuration: nil,
-                            rawRate: nil,
-                            absolute: nil,
-                            rate: nil,
-                            eventType: .nsSensorChange,
-                            createdAt: sessionStartDate,
-                            enteredBy: NightscoutTreatment.local,
-                            bolus: nil,
-                            insulin: nil,
-                            notes: notes,
-                            carbs: nil,
-                            fat: nil,
-                            protein: nil,
-                            targetTop: nil,
-                            targetBottom: nil
-                        )
-                        debug(.deviceManager, "CGM sensor change \(treatment)")
-                        treatments.append(treatment)
-                        updated = true
-                    }
-                    if updated {
-                        // We have to keep quite a bit of history as sensors start only every 10 days.
-                        storage.save(
-                            treatments.filter
-                                { $0.createdAt != nil && $0.createdAt!.addingTimeInterval(30.days.timeInterval) > Date() },
-                            as: file
-                        )
-                    }
+    private func storeCGMState(_ glucose: [BloodGlucose]) {
+        debug(.deviceManager, "start storage cgmState")
+        storage.transaction { storage in
+            let file = OpenAPS.Monitor.cgmState
+            var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
+            var updated = false
+
+            for x in glucose {
+                guard let sessionStartDate = x.sessionStartDate else { continue }
+
+                // Skip if we already have a recent treatment
+                if let lastTreatment = treatments.last,
+                   let createdAt = lastTreatment.createdAt,
+                   abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
+                {
+                    continue
                 }
                 }
+
+                let notes = createCGMStateNotes(transmitterID: x.transmitterID, activationDate: x.activationDate)
+                let treatment = createCGMStateTreatment(sessionStartDate: sessionStartDate, notes: notes)
+
+                debug(.deviceManager, "CGM sensor change \(treatment)")
+                treatments.append(treatment)
+                updated = true
+            }
+
+            if updated {
+                storage.save(
+                    treatments.filter { $0.createdAt?.addingTimeInterval(30.days.timeInterval) ?? .distantPast > Date() },
+                    as: file
+                )
             }
             }
         }
         }
     }
     }
 
 
+    private func createCGMStateNotes(transmitterID: String?, activationDate: Date?) -> String {
+        var notes = ""
+        if let t = transmitterID {
+            notes = t
+        }
+        if let a = activationDate {
+            notes = "\(notes) activated on \(a)"
+        }
+        return notes
+    }
+
+    private func createCGMStateTreatment(sessionStartDate: Date, notes: String) -> NightscoutTreatment {
+        NightscoutTreatment(
+            duration: nil,
+            rawDuration: nil,
+            rawRate: nil,
+            absolute: nil,
+            rate: nil,
+            eventType: .nsSensorChange,
+            createdAt: sessionStartDate,
+            enteredBy: NightscoutTreatment.local,
+            bolus: nil,
+            insulin: nil,
+            notes: notes,
+            carbs: nil,
+            fat: nil,
+            protein: nil,
+            targetTop: nil,
+            targetBottom: nil
+        )
+    }
+
     func addManualGlucose(glucose: Int) {
     func addManualGlucose(glucose: Int) {
         coredataContext.perform {
         coredataContext.perform {
             let newItem = GlucoseStored(context: self.coredataContext)
             let newItem = GlucoseStored(context: self.coredataContext)
@@ -190,7 +233,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 try self.coredataContext.save()
                 try self.coredataContext.save()
 
 
                 // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
                 // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
-                self.updateSubject.send(())
+                self.updateSubject.send()
             } catch let error as NSError {
             } catch let error as NSError {
                 debugPrint(
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save manual glucose to Core Data with error: \(error)"
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save manual glucose to Core Data with error: \(error)"
@@ -205,22 +248,30 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
     }
 
 
     func syncDate() -> Date {
     func syncDate() -> Date {
-        let fr = GlucoseStored.fetchRequest()
+        // Optimize fetch request to only get the date
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        let fr = NSFetchRequest<NSDictionary>(entityName: "GlucoseStored")
         fr.predicate = NSPredicate.predicateForOneDayAgo
         fr.predicate = NSPredicate.predicateForOneDayAgo
-        fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
+        fr.propertiesToFetch = ["date"]
         fr.fetchLimit = 1
         fr.fetchLimit = 1
+        fr.resultType = .dictionaryResultType
+        fr.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
 
 
-        var date: Date?
-        coredataContext.performAndWait {
+        var fetchedDate: Date = .distantPast
+
+        taskContext.performAndWait {
             do {
             do {
-                let results = try self.coredataContext.fetch(fr)
-                date = results.first?.date
-            } catch let error as NSError {
-                print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
+                if let result = try taskContext.fetch(fr).first,
+                   let date = result["date"] as? Date
+                {
+                    fetchedDate = date
+                }
+            } catch {
+                debugPrint("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription)")
             }
             }
         }
         }
 
 
-        return date ?? .distantPast
+        return fetchedDate
     }
     }
 
 
     func lastGlucoseDate() -> Date {
     func lastGlucoseDate() -> Date {
@@ -262,9 +313,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return filtered
         return filtered
     }
     }
 
 
-    func fetchLatestGlucose() -> GlucoseStored? {
+    func fetchLatestGlucose() throws -> GlucoseStored? {
         let predicate = NSPredicate.predicateFor20MinAgo
         let predicate = NSPredicate.predicateFor20MinAgo
-        return (CoreDataStack.shared.fetchEntities(
+        return (try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: predicate,
             predicate: predicate,
@@ -276,8 +327,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch glucose that is not uploaded to Nightscout yet
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
     /// - 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,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
@@ -285,8 +336,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await coredataContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
@@ -307,8 +360,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
     /// - 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,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
@@ -316,9 +369,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
                     duration: nil,
                     duration: nil,
@@ -361,8 +416,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch glucose that is not uploaded to Nightscout yet
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
     /// - 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,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
@@ -370,9 +425,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -391,8 +448,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
     /// - 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,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
@@ -400,9 +457,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -421,8 +480,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch glucose that is not uploaded to Tidepool yet
     // Fetch glucose that is not uploaded to Tidepool yet
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
     /// - 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,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
@@ -430,9 +489,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -452,8 +513,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch manual glucose that is not uploaded to Tidepool yet
     // Fetch manual glucose that is not uploaded to Tidepool yet
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
     /// - 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,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
@@ -461,9 +522,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -510,19 +573,24 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     var alarm: GlucoseAlarm? {
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         coredataContext.performAndWait {
         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
+            }
         }
         }
     }
     }
 }
 }
@@ -538,9 +606,9 @@ enum GlucoseAlarm {
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
         case .high:
         case .high:
-            return NSLocalizedString("LOWALERT!", comment: "LOWALERT!")
+            return String(localized: "LOWALERT!", comment: "LOWALERT!")
         case .low:
         case .low:
-            return NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!")
+            return String(localized: "HIGHALERT!", comment: "HIGHALERT!")
         }
         }
     }
     }
 }
 }

+ 57 - 49
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -3,17 +3,17 @@ import Foundation
 import Swinject
 import Swinject
 
 
 protocol OverrideStorage {
 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 calculateTarget(override: OverrideStored) -> Decimal
-    func storeOverride(override: Override) async
+    func storeOverride(override: Override) async throws
     func copyRunningOverride(_ override: OverrideStored) async -> NSManagedObjectID
     func copyRunningOverride(_ override: OverrideStored) async -> NSManagedObjectID
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     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 {
 final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
@@ -34,8 +34,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return dateFormatter
         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,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(
             predicate: NSPredicate(
@@ -47,15 +47,17 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    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,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
             predicate: NSPredicate.lastActiveOverride,
@@ -64,16 +66,18 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: fetchLimit
             fetchLimit: fetchLimit
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
     /// Returns the NSManagedObjectID of the Override Presets
     /// 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,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
             predicate: NSPredicate.allOverridePresets,
@@ -81,8 +85,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
@@ -95,14 +101,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return overrideTarget.decimalValue
         return overrideTarget.decimalValue
     }
     }
 
 
-    func storeOverride(override: Override) async {
+    func storeOverride(override: Override) async throws {
         var presetCount = -1
         var presetCount = -1
         if override.isPreset {
         if override.isPreset {
-            let presets = await fetchForOverridePresets()
+            let presets = try await fetchForOverridePresets()
             presetCount = presets.count
             presetCount = presets.count
         }
         }
 
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newOverride = OverrideStored(context: self.backgroundContext)
             let newOverride = OverrideStored(context: self.backgroundContext)
 
 
             // override key meta data
             // override key meta data
@@ -151,14 +157,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                 newOverride.smbIsScheduledOff = false
                 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 +206,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
         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,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
@@ -215,8 +215,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedOverrides = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedOverrides.map { override in
             return fetchedOverrides.map { override in
                 let duration = override.indefinite ? 43200 : override.duration ?? 0 // 43200 min = 30 days
                 let duration = override.indefinite ? 43200 : override.duration ?? 0 // 43200 min = 30 days
@@ -225,15 +227,15 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                     eventType: OverrideStored.EventType.nsExercise,
                     eventType: OverrideStored.EventType.nsExercise,
                     createdAt: override.date ?? Date(),
                     createdAt: override.date ?? Date(),
                     enteredBy: NightscoutExercise.local,
                     enteredBy: NightscoutExercise.local,
-                    notes: override.name ?? "Custom Override",
+                    notes: override.name ?? String(localized: "Custom Override"),
                     id: UUID(uuidString: override.id ?? UUID().uuidString)
                     id: UUID(uuidString: override.id ?? UUID().uuidString)
                 )
                 )
             }
             }
         }
         }
     }
     }
 
 
-    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,
             ofType: OverrideRunStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(
             predicate: NSPredicate(
@@ -245,8 +247,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedOverrideRuns = results as? [OverrideRunStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedOverrideRuns.map { overrideRun in
             return fetchedOverrideRuns.map { overrideRun in
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
@@ -256,15 +260,15 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                     eventType: OverrideStored.EventType.nsExercise,
                     eventType: OverrideStored.EventType.nsExercise,
                     createdAt: (overrideRun.startDate ?? overrideRun.override?.date) ?? Date(),
                     createdAt: (overrideRun.startDate ?? overrideRun.override?.date) ?? Date(),
                     enteredBy: NightscoutExercise.local,
                     enteredBy: NightscoutExercise.local,
-                    notes: overrideRun.name ?? "Custom Override",
+                    notes: overrideRun.name ?? String(localized: "Custom Override"),
                     id: overrideRun.id
                     id: overrideRun.id
                 )
                 )
             }
             }
         }
         }
     }
     }
 
 
-    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,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
             predicate: NSPredicate.allOverridePresets,
@@ -272,8 +276,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map { overrideStored in
             return fetchedResults.map { overrideStored in
                 let duration = overrideStored.duration as? Decimal != 0 ? overrideStored.duration as? Decimal : nil
                 let duration = overrideStored.duration as? Decimal != 0 ? overrideStored.duration as? Decimal : nil
@@ -290,8 +296,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,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
             predicate: NSPredicate.lastActiveOverride,
@@ -300,10 +306,12 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
+        return try await backgroundContext.perform {
             guard let fetchedResults = results as? [OverrideStored],
             guard let fetchedResults = results as? [OverrideStored],
                   let latestOverride = fetchedResults.first
                   let latestOverride = fetchedResults.first
-            else { return nil }
+            else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return latestOverride.objectID
             return latestOverride.objectID
         }
         }

+ 221 - 202
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -11,13 +11,12 @@ protocol PumpHistoryObserver {
 
 
 protocol PumpHistoryStorage {
 protocol PumpHistoryStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storePumpEvents(_ events: [NewPumpEvent])
+    func getPumpHistory() async throws -> [PumpHistoryEvent]
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
-    func recent() -> [PumpHistoryEvent]
-    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
-    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
-    func deleteInsulin(at date: Date)
+    func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getPumpHistoryNotYetUploadedToHealth() async throws -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async throws -> [PumpHistoryEvent]
 }
 }
 
 
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
@@ -27,6 +26,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var settings: SettingsManager!
     @Injected() private var settings: SettingsManager!
 
 
     private let updateSubject = PassthroughSubject<Void, Never>()
     private let updateSubject = PassthroughSubject<Void, Never>()
+    private let context = CoreDataStack.shared.newTaskContext()
 
 
     var updatePublisher: AnyPublisher<Void, Never> {
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
         updateSubject.eraseToAnyPublisher()
@@ -39,190 +39,184 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     typealias PumpEvent = PumpEventStored.EventType
     typealias PumpEvent = PumpEventStored.EventType
     typealias TempType = PumpEventStored.TempType
     typealias TempType = PumpEventStored.TempType
 
 
-    private let context = CoreDataStack.shared.newTaskContext()
-
     private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
     private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
         let roundedValue = (dose / increment).rounded() * increment
         let roundedValue = (dose / increment).rounded() * increment
         return Decimal(roundedValue)
         return Decimal(roundedValue)
     }
     }
 
 
-    func storePumpEvents(_ events: [NewPumpEvent]) {
-        processQueue.async {
-            self.context.perform {
-                for event in events {
-                    // Fetch to filter out duplicates
-                    // TODO: - move this to the Core Data Class
-
-                    let existingEvents: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
-                        ofType: PumpEventStored.self,
-                        onContext: self.context,
-                        predicate: NSPredicate.duplicateInLastHour(event.date),
-                        key: "timestamp",
-                        ascending: false,
-                        batchSize: 50
-                    ) as? [PumpEventStored] ?? []
-
-                    switch event.type {
-                    case .bolus:
-
-                        guard let dose = event.dose else { continue }
-                        let amount = self.roundDose(
-                            dose.unitsInDeliverableIncrements,
-                            toIncrement: Double(self.settings.preferences.bolusIncrement)
-                        )
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws {
+        try await context.perform {
+            for event in events {
+                let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
+                    ofType: PumpEventStored.self,
+                    onContext: self.context,
+                    predicate: NSPredicate.duplicateInLastHour(event.date),
+                    key: "timestamp",
+                    ascending: false,
+                    batchSize: 50
+                ) as? [PumpEventStored] ?? []
 
 
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-
-                            if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
-                                if existingEvent.timestamp == event.date {
-                                    if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
-                                        // Update existing event with new smaller value
-                                        existingEvent.bolus?.amount = amount as NSDecimalNumber
-                                        existingEvent.bolus?.isSMB = dose.automatic ?? true
-                                        existingEvent.isUploadedToNS = false
-                                        existingEvent.isUploadedToHealth = false
-                                        existingEvent.isUploadedToTidepool = false
-
-                                        print("Updated existing event with smaller value: \(amount)")
-                                    }
+                switch event.type {
+                case .bolus:
+
+                    guard let dose = event.dose else { continue }
+                    let amount = self.roundDose(
+                        dose.unitsInDeliverableIncrements,
+                        toIncrement: Double(self.settings.preferences.bolusIncrement)
+                    )
+
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+
+                        if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
+                            if existingEvent.timestamp == event.date {
+                                if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
+                                    // Update existing event with new smaller value
+                                    existingEvent.bolus?.amount = amount as NSDecimalNumber
+                                    existingEvent.bolus?.isSMB = dose.automatic ?? true
+                                    existingEvent.isUploadedToNS = false
+                                    existingEvent.isUploadedToHealth = false
+                                    existingEvent.isUploadedToTidepool = false
+
+                                    print("Updated existing event with smaller value: \(amount)")
                                 }
                                 }
                             }
                             }
-                            continue
                         }
                         }
+                        continue
+                    }
 
 
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        // restrict entry to now or past
-                        newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
-                        newPumpEvent.type = PumpEvent.bolus.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                        let newBolusEntry = BolusStored(context: self.context)
-                        newBolusEntry.pumpEvent = newPumpEvent
-                        newBolusEntry.amount = NSDecimalNumber(decimal: amount)
-                        newBolusEntry.isExternal = dose.manuallyEntered
-                        newBolusEntry.isSMB = dose.automatic ?? true
-
-                    case .tempBasal:
-                        guard let dose = event.dose else { continue }
-
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    // restrict entry to now or past
+                    newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
+                    newPumpEvent.type = PumpEvent.bolus.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                    let newBolusEntry = BolusStored(context: self.context)
+                    newBolusEntry.pumpEvent = newPumpEvent
+                    newBolusEntry.amount = NSDecimalNumber(decimal: amount)
+                    newBolusEntry.isExternal = dose.manuallyEntered
+                    newBolusEntry.isSMB = dose.automatic ?? true
+
+                case .tempBasal:
+                    guard let dose = event.dose else { continue }
+
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
 
 
-                        let rate = Decimal(dose.unitsPerHour)
-                        let minutes = (dose.endDate - dose.startDate).timeInterval / 60
-                        let delivered = dose.deliveredUnits
-                        let date = event.date
-
-                        let isCancel = delivered != nil
-                        guard !isCancel else { continue }
-
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = date
-                        newPumpEvent.type = PumpEvent.tempBasal.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                        let newTempBasal = TempBasalStored(context: self.context)
-                        newTempBasal.pumpEvent = newPumpEvent
-                        newTempBasal.duration = Int16(round(minutes))
-                        newTempBasal.rate = rate as NSDecimalNumber
-                        newTempBasal.tempType = TempType.absolute.rawValue
-
-                    case .suspend:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .resume:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.pumpResume.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .rewind:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.rewind.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .prime:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.prime.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .alarm:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-                        newPumpEvent.note = event.title
-
-                    default:
+                    let rate = Decimal(dose.unitsPerHour)
+                    let minutes = (dose.endDate - dose.startDate).timeInterval / 60
+                    let delivered = dose.deliveredUnits
+                    let date = event.date
+
+                    let isCancel = delivered != nil
+                    guard !isCancel else { continue }
+
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = date
+                    newPumpEvent.type = PumpEvent.tempBasal.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                    let newTempBasal = TempBasalStored(context: self.context)
+                    newTempBasal.pumpEvent = newPumpEvent
+                    newTempBasal.duration = Int16(round(minutes))
+                    newTempBasal.rate = rate as NSDecimalNumber
+                    newTempBasal.tempType = TempType.absolute.rawValue
+
+                case .suspend:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .resume:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.pumpResume.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .rewind:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.rewind.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .prime:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.prime.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .alarm:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
                         continue
                         continue
                     }
                     }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+                    newPumpEvent.note = event.title
+
+                default:
+                    continue
                 }
                 }
+            }
 
 
-                do {
-                    guard self.context.hasChanges else { return }
-                    try self.context.save()
+            do {
+                guard self.context.hasChanges else { return }
+                try self.context.save()
 
 
-                    self.updateSubject.send(())
-                    debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
-                } catch let error as NSError {
-                    debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
-                }
+                self.updateSubject.send(())
+                debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
+                throw error
             }
             }
         }
         }
     }
     }
@@ -258,21 +252,40 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
         }
     }
     }
 
 
-    func recent() -> [PumpHistoryEvent] {
-        storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
-    }
+    func getPumpHistory() async throws -> [PumpHistoryEvent] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpHistoryLast24h,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
 
 
-    func deleteInsulin(at date: Date) {
-        processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
-            guard let entryIndex = allValues.firstIndex(where: { $0.timestamp == date }) else {
-                return
-            }
-            allValues.remove(at: entryIndex)
-            storage.save(allValues, as: OpenAPS.Monitor.pumpHistory)
-            broadcaster.notify(PumpHistoryObserver.self, on: processQueue) {
-                $0.pumpHistoryDidUpdate(allValues)
-            }
+        return await context.perform {
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+            return fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEventStored.EventType.bolus.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .bolus,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.bolus?.amount as Decimal?
+                    )
+                case PumpEventStored.EventType.tempBasal.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .tempBasal,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.tempBasal?.rate as Decimal?,
+                        duration: Int(event.tempBasal?.duration ?? 0)
+                    )
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
         }
         }
     }
     }
 
 
@@ -286,8 +299,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
         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,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
@@ -295,8 +308,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await context.perform { [self] in
-            guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+        return try await context.perform { [self] in
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedPumpEvents.map { event in
             return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
@@ -445,8 +460,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,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
@@ -454,10 +469,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+        return try await context.perform {
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await context.perform {
-            fetchedPumpEvents.map { event in
+            return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
                 case PumpEvent.bolus.rawValue:
                 case PumpEvent.bolus.rawValue:
                     return PumpHistoryEvent(
                     return PumpHistoryEvent(
@@ -487,8 +504,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,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
@@ -496,10 +513,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+        return try await context.perform {
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await context.perform {
-            fetchedPumpEvents.map { event in
+            return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
                 case PumpEvent.bolus.rawValue:
                 case PumpEvent.bolus.rawValue:
                     return PumpHistoryEvent(
                     return PumpHistoryEvent(

+ 661 - 0
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -0,0 +1,661 @@
+import Foundation
+import LoopKitUI
+import Swinject
+
+protocol TDDStorage {
+    func calculateTDD(
+        pumpManager: any PumpManagerUI,
+        pumpHistory: [PumpHistoryEvent],
+        basalProfile: [BasalProfileEntry]
+    ) async throws
+        -> TDDResult
+    func storeTDD(_ tddResult: TDDResult) async
+}
+
+/// Structure containing the results of TDD calculations
+struct TDDResult {
+    let total: Decimal
+    let bolus: Decimal
+    let tempBasal: Decimal
+    let scheduledBasal: Decimal
+    let weightedAverage: Decimal?
+    let hoursOfData: Double
+}
+
+/// Implementation of the TDD Calculator
+final class BaseTDDStorage: TDDStorage, Injectable {
+    @Injected() private var storage: FileStorage!
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    private let privateContext = CoreDataStack.shared.newTaskContext()
+
+    /// Main function to calculate TDD from pump history and basal profile
+    /// - Parameters:
+    ///   - pumpManager: Representation of paired pump's PumpManagerUI
+    ///   - pumpHistory: Array of pump history events
+    ///   - basalProfile: Array of basal profile entries
+    /// - Returns: TDDResult containing all calculated values
+    func calculateTDD(
+        pumpManager: any PumpManagerUI,
+        pumpHistory: [PumpHistoryEvent],
+        basalProfile: [BasalProfileEntry]
+    ) async throws -> TDDResult {
+        debug(.apsManager, "Starting TDD calculation with \(pumpHistory.count) pump events")
+
+        // Log the first and last pump history events if available
+        let earliestEvent: String
+        let latestEvent: String
+
+        // We fetch descending, so invert logic
+        if let firstEvent = pumpHistory.last, let lastEvent = pumpHistory.first {
+            earliestEvent = "Type: \(firstEvent.type), Timestamp: \(firstEvent.timestamp.ISO8601Format())"
+            latestEvent = "Type: \(lastEvent.type), Timestamp: \(lastEvent.timestamp.ISO8601Format())"
+        } else {
+            earliestEvent = "No events available"
+            latestEvent = "No events available"
+            debug(.apsManager, "No pump history events available for logging.")
+        }
+
+        // Group events by type once to avoid multiple filters
+        let groupedEvents = Dictionary(grouping: pumpHistory, by: { $0.type })
+        let bolusEvents = groupedEvents[.bolus] ?? []
+        let tempBasalEvents = groupedEvents[.tempBasal] ?? []
+        let pumpSuspendEvents = groupedEvents[.pumpSuspend] ?? []
+        let pumpResumeEvents = groupedEvents[.pumpResume] ?? []
+
+        // Create pairs of suspend + resume events
+        let suspendResumePairs = zip(pumpSuspendEvents, pumpResumeEvents).filter { suspend, resume in
+            resume.timestamp > suspend.timestamp
+        }
+
+        // Calculate all components concurrently
+        async let pumpDataHours = calculatePumpDataHours(pumpHistory)
+        async let bolusInsulin = calculateBolusInsulin(bolusEvents)
+        let gaps = findBasalGaps(in: tempBasalEvents)
+        async let scheduledBasalInsulin = !gaps.isEmpty ? calculateScheduledBasalInsulin(
+            gaps: gaps,
+            profile: basalProfile,
+            roundToSupportedBasalRate: pumpManager.roundToSupportedBasalRate
+        ) : 0
+        async let tempBasalInsulin = calculateTempBasalInsulin(
+            tempBasalEvents, suspendResumePairs: suspendResumePairs,
+            roundToSupportedBasalRate: pumpManager.roundToSupportedBasalRate
+        )
+        async let weightedAverage = calculateWeightedAverage()
+
+        // Await all concurrent calculations
+        let (hours, bolus, scheduled, temp, weighted) = try await (
+            pumpDataHours,
+            bolusInsulin,
+            scheduledBasalInsulin,
+            tempBasalInsulin,
+            weightedAverage
+        )
+
+        // Total insulin calculation
+        let total = bolus + temp + scheduled
+
+        // Safeguard against division by zero
+        let percentage: (Decimal, Decimal) -> String = { part, total in
+            total > 0 ? String(format: "%.2f", NSDecimalNumber(decimal: (part / total * 100).rounded(toPlaces: 2)).doubleValue) :
+                "0.00"
+        }
+
+        // Store log strings in variables to avoid Xcode auto formatter from breaking up the lines in log statement
+        let totalString = String(format: "%.2f", NSDecimalNumber(decimal: total.rounded(toPlaces: 2)).doubleValue)
+        let bolusString = String(format: "%.2f", NSDecimalNumber(decimal: bolus.rounded(toPlaces: 2)).doubleValue)
+        let tempBasalString = String(format: "%.2f", NSDecimalNumber(decimal: temp.rounded(toPlaces: 2)).doubleValue)
+        let scheduledBasalString = String(format: "%.2f", NSDecimalNumber(decimal: scheduled.rounded(toPlaces: 2)).doubleValue)
+        let weightedAvgString = String(format: "%.2f", NSDecimalNumber(decimal: weighted?.rounded(toPlaces: 2) ?? 0))
+        let hoursString = String(format: "%.5f", NSDecimalNumber(decimal: Decimal(hours).truncated(toPlaces: 5)).doubleValue)
+
+        debug(.apsManager, """
+        TDD Summary:
+        +-------------------+-----------+-----------+
+        | Type\t\t\t\t| Amount U\t| Percent %\t|
+        +-------------------+-----------+-----------+
+        | Total\t\t\t\t| \(totalString)\t\t| \t\t\t|
+        | Bolus\t\t\t\t| \(bolusString)\t\t| \(percentage(bolus, total))\t\t|
+        | Temp Basal\t\t| \(tempBasalString)\t\t| \(percentage(temp, total))\t\t|
+        | Scheduled Basal\t| \(scheduledBasalString)\t\t| \(percentage(scheduled, total))\t\t|
+        | Weighted Average\t| \(weightedAvgString)\t\t| \t\t\t|
+        +-------------------+-----------+-----------+
+        - Hours of Data: \(hoursString)
+        - Earliest Event: \(earliestEvent)
+        - Latest Event: \(latestEvent)
+        """)
+
+        // Return calculated TDDResult
+        return TDDResult(
+            total: total,
+            bolus: bolus,
+            tempBasal: temp,
+            scheduledBasal: scheduled,
+            weightedAverage: weighted,
+            hoursOfData: hours
+        )
+    }
+
+    /// Stores the Total Daily Dose (TDD) result in Core Data
+    /// - Parameter tddResult: The TDD result to store, containing total insulin, bolus, temp basal, scheduled basal and weighted average
+    func storeTDD(_ tddResult: TDDResult) async {
+        await privateContext.perform {
+            let tddStored = TDDStored(context: self.privateContext)
+            tddStored.id = UUID()
+            tddStored.date = Date()
+            tddStored.total = NSDecimalNumber(decimal: tddResult.total)
+            tddStored.bolus = NSDecimalNumber(decimal: tddResult.bolus)
+            tddStored.tempBasal = NSDecimalNumber(decimal: tddResult.tempBasal)
+            tddStored.scheduledBasal = NSDecimalNumber(decimal: tddResult.scheduledBasal)
+            tddStored.weightedAverage = tddResult.weightedAverage.map { NSDecimalNumber(decimal: $0) }
+
+            do {
+                guard self.privateContext.hasChanges else { return }
+                try self.privateContext.save()
+            } catch {
+                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to save TDD: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    /// Calculates the number of hours of available pump history data
+    /// - Parameter pumpHistory: Array of pump history events
+    /// - Returns: Number of hours of available data
+    private func calculatePumpDataHours(_ pumpHistory: [PumpHistoryEvent]) -> Double {
+        guard let firstEvent = pumpHistory.last, // we are fetching in a descending order
+              let lastEvent = pumpHistory.first
+        else {
+            return 0
+        }
+
+        let startDate = firstEvent.timestamp
+        var endDate = lastEvent.timestamp
+
+        // If last event in the list is tempBasal, check if it is running longer than current time
+        // If yes, set current date, else ignore
+        if lastEvent.type == .tempBasal, lastEvent.timestamp > Date().addingTimeInterval(-1) {
+            endDate = Date()
+        }
+
+        return Double(endDate.timeIntervalSince(startDate)) / 3600.0
+    }
+
+    /// Calculates total bolus insulin from pump history
+    /// - Parameter bolusEvents: Array of pump history events of type bolus
+    /// - Returns: Total bolus insulin
+    private func calculateBolusInsulin(_ bolusEvents: [PumpHistoryEvent]) -> Decimal {
+        bolusEvents
+            .reduce(Decimal(0)) { totalBolusInsulin, event in
+//                let newTotalBolusInsulin =
+                totalBolusInsulin + (event.amount as Decimal? ?? 0)
+//                debug(
+//                    .apsManager,
+//                    "Bolus \(event.amount ?? 0) U dosed at \(event.timestamp.ISO8601Format()) added. New total bolus = \(newTotalBolusInsulin) U"
+//                )
+//                return newTotalBolusInsulin
+            }
+    }
+
+    /// Calculates temporary basal insulin delivery for a given time period, accounting for interruptions and suspensions
+    /// - Parameters:
+    ///   - tempBasalEvents: Array of temporary basal events
+    ///   - suspendResumePairs: Array of suspend and resume event pairs
+    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
+    /// - Returns: Total insulin delivered via temporary basal rates in units
+    private func calculateTempBasalInsulin(
+        _ tempBasalEvents: [PumpHistoryEvent],
+        suspendResumePairs: [(suspend: PumpHistoryEvent, resume: PumpHistoryEvent)],
+        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
+    ) -> Decimal {
+        guard !tempBasalEvents.isEmpty else { return 0 }
+
+        // Merge temp basal events and suspend-resume pairs into a single timeline
+        var timeline = [(start: Date, end: Date, type: EventType, rate: Decimal?)]()
+
+        // Add temp basal events to the timeline
+        for event in tempBasalEvents {
+            guard let duration = event.duration, let rate = event.amount else { continue }
+            let end = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
+            timeline.append((start: event.timestamp, end: end, type: .tempBasal, rate: rate))
+        }
+
+        // Add suspend-resume events to the timeline
+        for suspendResume in suspendResumePairs {
+            timeline.append((
+                start: suspendResume.suspend.timestamp,
+                end: suspendResume.resume.timestamp,
+                type: .pumpSuspend,
+                rate: nil
+            ))
+        }
+
+        // Sort the timeline by start time
+        timeline.sort { $0.start < $1.start }
+
+        // Calculate insulin delivery while accounting for suspensions and premature interruptions
+        var totalInsulin: Decimal = 0
+        let currentDate = Date()
+        var lastEndTime: Date?
+
+        for (index, event) in timeline.enumerated() {
+            if event.type == .tempBasal {
+                let effectiveEnd = min(event.end, currentDate) // Adjust for ongoing temp basals
+                var actualStart = event.start
+                var actualEnd = effectiveEnd
+
+                // Check for interruption by the next event
+                if index < timeline.count - 1 {
+                    let nextEvent = timeline[index + 1]
+                    if nextEvent.start < actualEnd, nextEvent.type != .pumpSuspend {
+                        actualEnd = nextEvent.start
+                    }
+                }
+
+                // Adjust for overlapping suspensions
+                if let lastSuspendEnd = lastEndTime, lastSuspendEnd > actualStart {
+                    actualStart = lastSuspendEnd
+                }
+
+                // Calculate insulin if the duration is valid
+                let durationMinutes = max(0, actualEnd.timeIntervalSince(actualStart) / 60)
+                if durationMinutes > 0, let rate = event.rate {
+                    let durationHours = (Decimal(durationMinutes) / 60).truncated(toPlaces: 5)
+                    let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
+                    if insulin > 0 {
+                        totalInsulin += insulin
+
+//                        debug(
+//                            .apsManager,
+//                            "Temp basal: \(rate) U/hr for \(durationHours) hr (Start: \(actualStart.ISO8601Format()), End: \(actualEnd.ISO8601Format())) = \(insulin) U"
+//                        )
+                    }
+                }
+            } else if event.type == .pumpSuspend {
+                // Update the last suspend end time to adjust future temp basal durations
+                lastEndTime = event.end
+            }
+        }
+
+        return totalInsulin
+    }
+
+    /// Calculates scheduled basal insulin delivery during gaps between temporary basals
+    /// - Parameters:
+    ///   - gaps: Array of time periods where scheduled basal was active
+    ///   - profile: Basal profile entries defining rates throughout the day
+    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
+    /// - Returns: Total insulin delivered via scheduled basal in units
+    private func calculateScheduledBasalInsulin(
+        gaps: [(start: Date, end: Date)],
+        profile: [BasalProfileEntry],
+        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
+    ) -> Decimal {
+        // Initialize cached formatter for time string conversion
+        let timeFormatter: DateFormatter = {
+            let formatter = DateFormatter()
+            formatter.dateFormat = "HH:mm:ss"
+            return formatter
+        }()
+
+        // Pre-calculate profile switch times for efficient lookup
+        let profileSwitches = profile.map(\.minutes)
+
+        return gaps.reduce(into: Decimal(0)) { totalInsulin, gap in
+            var currentTime = gap.start
+            let now = Date()
+
+            while currentTime < gap.end {
+                // Find applicable basal rate for current time
+                guard let rate = findBasalRate(
+                    for: timeFormatter.string(from: currentTime),
+                    in: profile
+                ) else { break }
+
+                // Determine when rate changes (either profile switch or gap end)
+                let nextSwitchTime = getNextBasalRateSwitch(
+                    after: currentTime,
+                    switches: profileSwitches,
+                    calendar: Calendar.current
+                ) ?? gap.end
+
+                // Ensure endTime does not exceed current time or gap end
+                let endTime = min(min(nextSwitchTime, gap.end), now)
+
+                // Only proceed if we have a valid time interval
+                guard endTime > currentTime else { break }
+
+                let durationHours = (Decimal(endTime.timeIntervalSince(currentTime)) / 3600).truncated(toPlaces: 5)
+                let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
+
+                if insulin > 0 {
+                    totalInsulin += insulin
+
+//                    debug(
+//                        .apsManager,
+//                        "Scheduled Insulin added: \(insulin) U. Duration: \(durationHours) hrs (Start: \(currentTime.ISO8601Format()), End: \(endTime.ISO8601Format()))"
+//                    )
+                }
+
+                currentTime = endTime
+            }
+        }
+    }
+
+    /// Finds gaps between tempBasal events where scheduled basal ran
+    /// - Parameter tempBasalEvents: Array of pump history events of type tempBasal
+    /// - Returns: Array of gaps, where each gap has a start and end time
+    private func findBasalGaps(in tempBasalEvents: [PumpHistoryEvent]) -> [(start: Date, end: Date)] {
+        guard !tempBasalEvents.isEmpty else {
+            let startOfDay = Calendar.current.startOfDay(for: Date())
+            return [(start: startOfDay, end: startOfDay.addingTimeInterval(24 * 60 * 60 - 1))]
+        }
+
+        // Pre-sort events and create array with capacity
+        let sortedEvents = tempBasalEvents.sorted { $0.timestamp < $1.timestamp }
+        var gaps = [(start: Date, end: Date)]()
+        gaps.reserveCapacity(sortedEvents.count + 1)
+
+        // Use first event's date for calendar operations
+        let startOfDay = Calendar.current.startOfDay(for: sortedEvents.first!.timestamp)
+        let endOfDay = startOfDay.addingTimeInterval(24 * 60 * 60 - 1)
+
+        // Process events in a single pass
+        var lastEndTime = sortedEvents.first!.timestamp
+
+        for i in 0 ..< sortedEvents.count {
+            let event = sortedEvents[i]
+            guard let duration = event.duration else { continue }
+
+            // Calculate end time for current event
+            var currentEndTime = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
+
+            // Check for cancellation by next event
+            if i < sortedEvents.count - 1 {
+                let nextEvent = sortedEvents[i + 1]
+                if nextEvent.timestamp < currentEndTime {
+                    currentEndTime = nextEvent.timestamp
+                }
+            }
+
+            // Record gap if exists
+            if event.timestamp > lastEndTime {
+                gaps.append((start: lastEndTime, end: event.timestamp))
+            }
+
+            lastEndTime = currentEndTime
+        }
+
+        // Add final gap if needed
+        if lastEndTime < endOfDay {
+            gaps.append((start: lastEndTime, end: endOfDay))
+        }
+
+        return gaps
+    }
+
+//    /// Finds gaps between tempBasal events where scheduled basal ran, excluding suspend-resume periods
+//    /// - Parameters:
+//    ///   - tempBasalEvents: Array of pump history events of type tempBasal
+//    ///   - suspendResumePairs: Array of suspend and resume event pairs
+//    /// - Returns: Array of gaps, where each gap has a start and end time
+//    private func findBasalGaps(
+//        in tempBasalEvents: [PumpHistoryEvent],
+//        excluding suspendResumePairs: [(suspend: PumpHistoryEvent, resume: PumpHistoryEvent)]
+//    ) -> [(start: Date, end: Date)] {
+//        guard !tempBasalEvents.isEmpty else {
+//            let startOfDay = Calendar.current.startOfDay(for: Date())
+//            return [(start: startOfDay, end: startOfDay.addingTimeInterval(24 * 60 * 60 - 1))]
+//        }
+//
+//        // Merge temp basal and suspend-resume events into a unified timeline
+//        var timeline = [(start: Date, end: Date, type: EventType)]()
+//
+//        for event in tempBasalEvents {
+//            guard let duration = event.duration else { continue }
+//            let eventEnd = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
+//            timeline.append((start: event.timestamp, end: eventEnd, type: .tempBasal))
+//        }
+//
+//        for suspendResume in suspendResumePairs {
+//            timeline.append((start: suspendResume.suspend.timestamp, end: suspendResume.resume.timestamp, type: .pumpSuspend))
+//        }
+//
+//        // Sort the timeline by start time
+//        timeline.sort { $0.start < $1.start }
+//
+//        // Process the timeline to calculate gaps
+//        var gaps = [(start: Date, end: Date)]()
+//        var lastEndTime = Calendar.current.startOfDay(for: timeline.first!.start)
+//        let endOfDay = lastEndTime.addingTimeInterval(24 * 60 * 60 - 1)
+//
+//        for interval in timeline {
+//            if interval.type == .pumpSuspend {
+//                // Extend lastEndTime for suspend periods
+//                lastEndTime = max(lastEndTime, interval.end)
+//                continue
+//            }
+//
+//            if interval.start > lastEndTime {
+//                // Add a gap if there is a gap between lastEndTime and interval.start
+//                gaps.append((start: lastEndTime, end: interval.start))
+//            }
+//
+//            // Update lastEndTime to the maximum end time encountered
+//            lastEndTime = max(lastEndTime, interval.end)
+//        }
+//
+//        if lastEndTime < endOfDay {
+//            // Add a final gap if the lastEndTime is before the end of the day
+//            gaps.append((start: lastEndTime, end: endOfDay))
+//        }
+//
+//        return gaps
+//    }
+
+//    /// Calculates scheduled basal insulin delivery during gaps between temporary basals
+//    /// - Parameters:
+//    ///   - gaps: Array of time periods where scheduled basal was active
+//    ///   - profile: Basal profile entries defining rates throughout the day
+//    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
+//    /// - Returns: Total insulin delivered via scheduled basal in units
+//    private func calculateScheduledBasalInsulin(
+//        gaps: [(start: Date, end: Date)],
+//        profile: [BasalProfileEntry],
+//        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
+//    ) -> Decimal {
+//        // Initialize cached formatter for time string conversion
+//        let timeFormatter: DateFormatter = {
+//            let formatter = DateFormatter()
+//            formatter.dateFormat = "HH:mm:ss"
+//            return formatter
+//        }()
+//
+//        // Pre-calculate profile switch times for efficient lookup
+//        let profileSwitches = profile.map(\.minutes)
+//
+//        return gaps.reduce(into: Decimal(0)) { totalInsulin, gap in
+//            var currentTime = gap.start
+//
+//            while currentTime < gap.end {
+//                // Find applicable basal rate for the current time
+//                guard let rate = findBasalRate(
+//                    for: timeFormatter.string(from: currentTime),
+//                    in: profile
+//                ) else { break }
+//
+//                // Determine when the rate changes (profile switch or gap end)
+//                let nextSwitchTime = getNextBasalRateSwitch(
+//                    after: currentTime,
+//                    switches: profileSwitches,
+//                    calendar: Calendar.current
+//                ) ?? gap.end
+//                let endTime = min(nextSwitchTime, gap.end)
+//                let durationHours = Decimal(endTime.timeIntervalSince(currentTime)) / 3600
+//
+//                let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
+//                totalInsulin += insulin
+//
+//                debug(
+//                    .apsManager,
+//                    "Scheduled Insulin added: \(insulin) U. Duration: \(durationHours) hrs (\(currentTime)-\(endTime))"
+//                )
+//
+//                currentTime = endTime
+//            }
+//        }
+//    }
+
+    /// Finds the next basal rate switch time after a given time
+    /// - Parameters:
+    ///   - time: Reference time to find next switch after
+    ///   - switches: Pre-calculated array of minutes when profile rates change
+    ///   - calendar: Calendar instance for date calculations
+    /// - Returns: Date of next basal rate switch, or nil if none found
+    private func getNextBasalRateSwitch(
+        after time: Date,
+        switches: [Int],
+        calendar: Calendar
+    ) -> Date? {
+        let timeMinutes = calendar.component(.hour, from: time) * 60 + calendar.component(.minute, from: time)
+
+        // Find first switch time after current time
+        guard let nextSwitch = switches.first(where: { $0 > timeMinutes }) else {
+            return nil
+        }
+
+        // Convert switch time to absolute date
+        return calendar.startOfDay(for: time).addingTimeInterval(TimeInterval(nextSwitch * 60))
+    }
+
+    /// Finds the basal rate for a specific time using binary search
+    /// - Parameters:
+    ///   - timeString: Time in format "HH:mm:ss"
+    ///   - profile: Array of basal profile entries sorted by time
+    /// - Returns: Basal rate in units per hour, or nil if not found
+    private func findBasalRate(for timeString: String, in profile: [BasalProfileEntry]) -> Decimal? {
+        // Parse time string in "HH:mm:ss" format into hours and minutes components
+        let timeComponents = timeString.split(separator: ":")
+        guard timeComponents.count == 3,
+              let hours = Int(timeComponents[0]),
+              let minutes = Int(timeComponents[1])
+        else { return nil }
+
+        // Convert time to total minutes since midnight for easier comparison
+        let totalMinutes = hours * 60 + minutes
+
+        // Special case: If profile has only one entry, it applies for full 24 hours
+        // Return its rate immediately without searching
+        if profile.count == 1 {
+            return profile[0].rate
+        }
+
+        // Use binary search to efficiently find the applicable basal rate
+        // Profile entries are sorted by minutes, so we can divide and conquer
+        var left = 0
+        var right = profile.count - 1
+
+        while left <= right {
+            let mid = (left + right) / 2
+            let entry = profile[mid]
+            // Get end time for current entry - either next entry's start time or end of day (1440 mins)
+            let nextMinutes = mid + 1 < profile.count ? profile[mid + 1].minutes : 1440
+
+            // Check if target time falls within current entry's time range
+            if totalMinutes >= entry.minutes, totalMinutes < nextMinutes {
+                return entry.rate
+            }
+
+            // Adjust search range based on comparison
+            if totalMinutes < entry.minutes {
+                right = mid - 1 // Search in left half if target time is earlier
+            } else {
+                left = mid + 1 // Search in right half if target time is later
+            }
+        }
+
+        // No applicable rate found for the given time
+        return nil
+    }
+
+    /// Calculates a weighted average of Total Daily Dose (TDD) based on recent and historical data
+    ///
+    /// The weighted average is calculated using two time periods:
+    /// - Recent: Last 2 hours of TDD data
+    /// - Historical: Last 10 days of TDD data
+    ///
+    /// The formula used is:
+    /// ```
+    /// weightedTDD = (weightPercentage × recent_average) + ((1 - weightPercentage) × historical_average)
+    /// ```
+    /// where weightPercentage defaults to 0.65 if not set in preferences
+    ///
+    /// - Returns: A weighted average of TDD as Decimal, or nil if insufficient data
+    /// - Note: The weight percentage can be configured in preferences. Default is 0.65 (65% recent, 35% historical)
+    private func calculateWeightedAverage() async throws -> Decimal? {
+        // Fetch data from Core Data
+        let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
+        let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
+
+        let predicate = NSPredicate(format: "date >= %@", tenDaysAgo as NSDate)
+
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TDDStored.self,
+            onContext: privateContext,
+            predicate: predicate,
+            key: "date",
+            ascending: false
+        )
+        return await privateContext.perform { () -> Decimal? in
+            guard let results = results as? [TDDStored], !results.isEmpty else { return 0 }
+
+            // Calculate recent (2h) average
+            let recentResults = results.filter { $0.date?.timeIntervalSince(twoHoursAgo) ?? 0 > 0 }
+            let recentTotal = recentResults.compactMap { $0.total?.decimalValue }.reduce(0, +)
+            let recentCount = max(Decimal(recentResults.count), 1)
+            let averageTDDLastTwoHours = recentTotal / recentCount
+
+            // Calculate 10-day average
+            let totalTDD = results.compactMap { $0.total?.decimalValue }.reduce(0, +)
+            let totalCount = max(Decimal(results.count), 1)
+            let averageTDDLastTenDays = totalTDD / totalCount
+
+            // Get weight percentage from preferences (default 0.65 if not set)
+            let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
+            let weightPercentage = userPreferences?.weightPercentage ?? Decimal(0.65) // why is this 1 as default in oref2??
+
+            // Calculate weighted average using the formula:
+            // weightedTDD = (weightPercentage × recent_average) + ((1 - weightPercentage) × historical_average)
+            let weightedTDD = weightPercentage * averageTDDLastTwoHours +
+                (1 - weightPercentage) * averageTDDLastTenDays
+
+            return weightedTDD.truncated(toPlaces: 3)
+        }
+    }
+}
+
+/// Extension for rounding Decimal numbers
+extension Decimal {
+    /// Rounds a decimal to specified number of places
+    /// - Parameter places: Number of decimal places
+    /// - Returns: Rounded decimal
+    func rounded(toPlaces places: Int) -> Decimal {
+        var value = self
+        var result = Decimal()
+        NSDecimalRound(&result, &value, places, .plain)
+        return result
+    }
+
+    /// Truncates the `Decimal` to the specified number of decimal places without rounding.
+    ///
+    /// - Parameter places: The number of decimal places to retain.
+    /// - Returns: A `Decimal` truncated to the specified precision.
+    func truncated(toPlaces places: Int) -> Decimal {
+        var original = self
+        var result = Decimal()
+        NSDecimalRound(&result, &original, places, .down)
+        return result
+    }
+}

+ 49 - 38
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -8,18 +8,18 @@ protocol TempTargetsObserver {
 }
 }
 
 
 protocol TempTargetsStorage {
 protocol TempTargetsStorage {
-    func storeTempTarget(tempTarget: TempTarget) async
+    func storeTempTarget(tempTarget: TempTarget) async throws
     func saveTempTargetsToStorage(_ targets: [TempTarget])
     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 copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
     func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
     func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
-    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [TempTarget]
     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 presets() -> [TempTarget]
     func current() -> TempTarget?
     func current() -> TempTarget?
     func existsTempTarget(with date: Date) async -> Bool
     func existsTempTarget(with date: Date) async -> Bool
@@ -38,8 +38,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         injectServices(resolver)
         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,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveTempTarget,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -48,16 +48,18 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             fetchLimit: fetchLimit
             fetchLimit: fetchLimit
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
     /// Returns the NSManagedObjectID of the Temp Target Presets
     /// 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,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.allTempTargetPresets,
             predicate: NSPredicate.allTempTargetPresets,
@@ -65,17 +67,19 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
+    func fetchScheduledTempTargets() async throws -> [NSManagedObjectID] {
         let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
         let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
 
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: scheduledTempTargets,
             predicate: scheduledTempTargets,
@@ -83,17 +87,19 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
+    func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
         let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
 
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: predicate,
             predicate: predicate,
@@ -102,21 +108,23 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
-            fetchedResults.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func storeTempTarget(tempTarget: TempTarget) async {
+    func storeTempTarget(tempTarget: TempTarget) async throws {
         var presetCount = -1
         var presetCount = -1
         if tempTarget.isPreset == true {
         if tempTarget.isPreset == true {
-            let presets = await fetchForTempTargetPresets()
+            let presets = try await fetchForTempTargetPresets()
             presetCount = presets.count
             presetCount = presets.count
         }
         }
 
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newTempTarget = TempTargetStored(context: self.backgroundContext)
             let newTempTarget = TempTargetStored(context: self.backgroundContext)
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.id = UUID()
             newTempTarget.id = UUID()
@@ -144,9 +152,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
                 guard self.backgroundContext.hasChanges else { return }
                 guard self.backgroundContext.hasChanges else { return }
                 try self.backgroundContext.save()
                 try self.backgroundContext.save()
             } catch let error as NSError {
             } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target to Core Data with error: \(error.userInfo)"
-                )
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save new temp target with error: \(error.userInfo)")
+                throw error
             }
             }
         }
         }
     }
     }
@@ -242,8 +249,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return last
         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,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
@@ -251,8 +258,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedTempTargets = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedTempTargets = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedTempTargets.map { tempTarget in
             return fetchedTempTargets.map { tempTarget in
                 NightscoutTreatment(
                 NightscoutTreatment(
@@ -277,8 +286,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,
             ofType: TempTargetRunStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(
             predicate: NSPredicate(
@@ -290,8 +299,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedTempTargetRuns.map { tempTargetRun in
             return fetchedTempTargetRuns.map { tempTargetRun in
                 var durationInMinutes = (tempTargetRun.endDate?.timeIntervalSince(tempTargetRun.startDate ?? Date()) ?? 1) / 60
                 var durationInMinutes = (tempTargetRun.endDate?.timeIntervalSince(tempTargetRun.startDate ?? Date()) ?? 1) / 60

+ 17 - 3
Trio/Sources/Application/AppDelegate.swift

@@ -23,8 +23,15 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
             let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
             let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
 
 
             Task {
             Task {
-                await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
-                completionHandler(.newData)
+                do {
+                    try await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                    completionHandler(.newData)
+                } catch {
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) failed to handle remote notification with error: \(error.localizedDescription)"
+                    )
+                }
             }
             }
         } catch {
         } catch {
             debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
             debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
@@ -40,7 +47,14 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         let token = tokenParts.joined()
         let token = tokenParts.joined()
 
 
         Task {
         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)"
+                )
+            }
         }
         }
     }
     }
 
 

+ 20 - 11
Trio/Sources/Application/TrioApp.swift

@@ -63,19 +63,23 @@ import Swinject
     }
     }
 
 
     init() {
     init() {
+        let submodulesInfo = BuildDetails.default.submodules.map { key, value in
+            "\(key): \(value.branch) \(value.commitSHA)"
+        }.joined(separator: ", ")
+
         debug(
         debug(
             .default,
             .default,
-            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))]"
+            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))] [submodules: \(submodulesInfo)]"
         )
         )
 
 
         // Setup up the Core Data Stack
         // Setup up the Core Data Stack
         coreDataStack = CoreDataStack.shared
         coreDataStack = CoreDataStack.shared
 
 
+        // Explicitly initialize Core Data Stack
         do {
         do {
-            // Explicitly initialize Core Data Stacak
             try coreDataStack.initializeStack()
             try coreDataStack.initializeStack()
 
 
-            // Load services
+            // Only load services after successful Core Data initialization
             loadServices()
             loadServices()
 
 
             // Fix bug in iOS 18 related to the translucent tab bar
             // Fix bug in iOS 18 related to the translucent tab bar
@@ -84,12 +88,9 @@ import Swinject
             // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
             // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
             cleanupOldData()
             cleanupOldData()
         } catch {
         } catch {
-            debug(
-                .coreData,
-                "Failed to initialize Core Data Stack: \(error.localizedDescription)"
-            )
-            // Handle initialization failure
-            fatalError("Core Data Stack initialization failed: \(error.localizedDescription)")
+            debug(.coreData, "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)")
+
+            fatalError("Core Data Stack initialization failed")
         }
         }
     }
     }
 
 
@@ -109,6 +110,14 @@ import Swinject
             if newScenePhase == .background {
             if newScenePhase == .background {
                 coreDataStack.save()
                 coreDataStack.save()
             }
             }
+
+            if newScenePhase == .active {
+                if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+                   let rootVC = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController
+                {
+                    AppVersionChecker.shared.checkAndNotifyVersionStatus(in: rootVC)
+                }
+            }
         }
         }
         .backgroundTask(.appRefresh("com.trio.cleanup")) {
         .backgroundTask(.appRefresh("com.trio.cleanup")) {
             await scheduleDatabaseCleaning()
             await scheduleDatabaseCleaning()
@@ -142,9 +151,9 @@ import Swinject
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         do {
         do {
             try BGTaskScheduler.shared.submit(request)
             try BGTaskScheduler.shared.submit(request)
-            debugPrint("Task scheduled successfully")
+            debug(.coreData, "Task for cleaning database scheduled successfully")
         } catch {
         } catch {
-            debugPrint("Failed to schedule tasks")
+            debug(.coreData, "Failed to schedule tasks for cleaning database: \(error.localizedDescription)")
         }
         }
     }
     }
 
 

+ 1 - 0
Trio/Sources/Assemblies/StorageAssembly.swift

@@ -10,6 +10,7 @@ final class StorageAssembly: Assembly {
         container.register(PumpHistoryStorage.self) { r in BasePumpHistoryStorage(resolver: r) }
         container.register(PumpHistoryStorage.self) { r in BasePumpHistoryStorage(resolver: r) }
         container.register(OverrideStorage.self) { r in BaseOverrideStorage(resolver: r) }
         container.register(OverrideStorage.self) { r in BaseOverrideStorage(resolver: r) }
         container.register(DeterminationStorage.self) { r in BaseDeterminationStorage(resolver: r) }
         container.register(DeterminationStorage.self) { r in BaseDeterminationStorage(resolver: r) }
+        container.register(TDDStorage.self) { r in BaseTDDStorage(resolver: r) }
         container.register(GlucoseStorage.self) { r in BaseGlucoseStorage(resolver: r) }
         container.register(GlucoseStorage.self) { r in BaseGlucoseStorage(resolver: r) }
         container.register(TempTargetsStorage.self) { r in BaseTempTargetsStorage(resolver: r) }
         container.register(TempTargetsStorage.self) { r in BaseTempTargetsStorage(resolver: r) }
         container.register(CarbsStorage.self) { r in BaseCarbsStorage(resolver: r) }
         container.register(CarbsStorage.self) { r in BaseCarbsStorage(resolver: r) }

+ 20 - 11
Trio/Sources/Helpers/BuildDetails.swift

@@ -1,9 +1,3 @@
-//
-//  BuildDetails.swift
-//  Trio
-//
-//  Created by Jonas Björkert on 2024-05-09.
-//
 import Foundation
 import Foundation
 
 
 class BuildDetails {
 class BuildDetails {
@@ -14,7 +8,7 @@ class BuildDetails {
     init() {
     init() {
         guard let url = Bundle.main.url(forResource: "BuildDetails", withExtension: "plist"),
         guard let url = Bundle.main.url(forResource: "BuildDetails", withExtension: "plist"),
               let data = try? Data(contentsOf: url),
               let data = try? Data(contentsOf: url),
-              let parsed = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
+              let parsed = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any]
         else {
         else {
             dict = [:]
             dict = [:]
             return
             return
@@ -27,11 +21,26 @@ class BuildDetails {
     }
     }
 
 
     var branchAndSha: String {
     var branchAndSha: String {
-        let branch = dict["com-trio-branch"] as? String ?? "Unknown"
-        let sha = dict["com-trio-commit-sha"] as? String ?? "Unknown"
+        let branch = dict["com-trio-branch"] as? String ?? String(localized: "Unknown")
+        let sha = dict["com-trio-commit-sha"] as? String ?? String(localized: "Unknown")
         return "\(branch) \(sha)"
         return "\(branch) \(sha)"
     }
     }
 
 
+    /// Returns a dictionary of submodule details.
+    /// The keys are the submodule names, and the values are tuples (branch, commitSHA).
+    var submodules: [String: (branch: String, commitSHA: String)] {
+        guard let subs = dict["com-trio-submodules"] as? [String: [String: Any]] else {
+            return [:]
+        }
+        var result = [String: (branch: String, commitSHA: String)]()
+        for (name, info) in subs {
+            let branch = info["branch"] as? String ?? String(localized: "Unknown")
+            let commitSHA = info["commit_sha"] as? String ?? String(localized: "Unknown")
+            result[name] = (branch: branch, commitSHA: commitSHA)
+        }
+        return result
+    }
+
     // Determine if the build is from TestFlight
     // Determine if the build is from TestFlight
     func isTestFlightBuild() -> Bool {
     func isTestFlightBuild() -> Bool {
         #if targetEnvironment(simulator)
         #if targetEnvironment(simulator)
@@ -75,9 +84,9 @@ class BuildDetails {
     // Expiration header based on build type
     // Expiration header based on build type
     var expirationHeaderString: String {
     var expirationHeaderString: String {
         if isTestFlightBuild() {
         if isTestFlightBuild() {
-            return "Beta (TestFlight) Expires"
+            return String(localized: "Beta (TestFlight) Expires")
         } else {
         } else {
-            return "App Expires"
+            return String(localized: "App Expires")
         }
         }
     }
     }
 }
 }

+ 4 - 4
Trio/Sources/Helpers/HKUnit.swift

@@ -44,13 +44,13 @@ extension HKUnit {
 
 
     var localizedShortUnitString: String {
     var localizedShortUnitString: String {
         if self == HKUnit.millimolesPerLiter {
         if self == HKUnit.millimolesPerLiter {
-            return NSLocalizedString("mmol/L", comment: "The short unit display string for millimoles of glucose per liter")
+            return String(localized: "mmol/L", comment: "The short unit display string for millimoles of glucose per liter")
         } else if self == .milligramsPerDeciliter {
         } else if self == .milligramsPerDeciliter {
-            return NSLocalizedString("mg/dL", comment: "The short unit display string for milligrams of glucose per decilter")
+            return String(localized: "mg/dL", comment: "The short unit display string for milligrams of glucose per decilter")
         } else if self == .internationalUnit() {
         } else if self == .internationalUnit() {
-            return NSLocalizedString("U", comment: "The short unit display string for international units of insulin")
+            return String(localized: "U", comment: "The short unit display string for international units of insulin")
         } else if self == .gram() {
         } else if self == .gram() {
-            return NSLocalizedString("g", comment: "The short unit display string for grams")
+            return String(localized: "g", comment: "The short unit display string for grams")
         } else {
         } else {
             return String(describing: self)
             return String(describing: self)
         }
         }

+ 1 - 1
Trio/Sources/Helpers/MainChartHelper.swift

@@ -50,7 +50,7 @@ enum MainChartHelper {
     }
     }
 
 
     static func bolusOffset(units: GlucoseUnits) -> Decimal {
     static func bolusOffset(units: GlucoseUnits) -> Decimal {
-        units == .mgdL ? 30 : 1.66
+        units == .mgdL ? 20 : (20 / 18)
     }
     }
 
 
     static func calculateDuration(
     static func calculateDuration(

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 180375 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/ar.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1917
Trio/Sources/Localizations/Main/ca.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2184
Trio/Sources/Localizations/Main/da.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2229
Trio/Sources/Localizations/Main/de.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2231
Trio/Sources/Localizations/Main/en.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2228
Trio/Sources/Localizations/Main/es.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2229
Trio/Sources/Localizations/Main/fi.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2220
Trio/Sources/Localizations/Main/fr.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/he.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2178
Trio/Sources/Localizations/Main/hu.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2225
Trio/Sources/Localizations/Main/it.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/nb.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2231
Trio/Sources/Localizations/Main/nl.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2225
Trio/Sources/Localizations/Main/pl.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2226
Trio/Sources/Localizations/Main/ru.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2178
Trio/Sources/Localizations/Main/sk.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/sv.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2227
Trio/Sources/Localizations/Main/tr.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2223
Trio/Sources/Localizations/Main/uk.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2180
Trio/Sources/Localizations/Main/vi.lproj/Localizable.strings


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2225
Trio/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings


+ 4 - 0
Trio/Sources/Logger/Logger.swift

@@ -116,6 +116,7 @@ final class Logger {
     static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
     static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
     static let watchManager = Logger(category: .watchManager, reporter: baseReporter)
     static let watchManager = Logger(category: .watchManager, reporter: baseReporter)
     static let coreData = Logger(category: .coreData, reporter: baseReporter)
     static let coreData = Logger(category: .coreData, reporter: baseReporter)
+    static let storage = Logger(category: .storage, reporter: baseReporter)
 
 
     enum Category: String {
     enum Category: String {
         case `default`
         case `default`
@@ -129,6 +130,7 @@ final class Logger {
         case bolusState
         case bolusState
         case watchManager
         case watchManager
         case coreData
         case coreData
+        case storage
 
 
         var name: String {
         var name: String {
             rawValue.capitalizingFirstLetter()
             rawValue.capitalizingFirstLetter()
@@ -147,6 +149,7 @@ final class Logger {
             case .bolusState: return .bolusState
             case .bolusState: return .bolusState
             case .watchManager: return .watchManager
             case .watchManager: return .watchManager
             case .coreData: return .coreData
             case .coreData: return .coreData
+            case .storage: return .storage
             }
             }
         }
         }
 
 
@@ -163,6 +166,7 @@ final class Logger {
                  .openAPS,
                  .openAPS,
                  .remoteControl,
                  .remoteControl,
                  .service,
                  .service,
+                 .storage,
                  .watchManager:
                  .watchManager:
                 return OSLog(subsystem: subsystem, category: name)
                 return OSLog(subsystem: subsystem, category: name)
             }
             }

+ 16 - 16
Trio/Sources/Models/ContactTrickEntry.swift

@@ -130,23 +130,23 @@ enum ContactImageValue: String, JSON, CaseIterable, Identifiable, Codable {
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
         case .none:
         case .none:
-            return NSLocalizedString("None", comment: "")
+            return String(localized: "None", comment: "")
         case .glucose:
         case .glucose:
-            return NSLocalizedString("Glucose Reading", comment: "")
+            return String(localized: "Glucose Reading", comment: "")
         case .eventualBG:
         case .eventualBG:
-            return NSLocalizedString("Eventual Glucose", comment: "")
+            return String(localized: "Eventual Glucose", comment: "")
         case .delta:
         case .delta:
-            return NSLocalizedString("Glucose Delta", comment: "")
+            return String(localized: "Glucose Delta", comment: "")
         case .trend:
         case .trend:
-            return NSLocalizedString("Glucose Trend", comment: "")
+            return String(localized: "Glucose Trend", comment: "")
         case .lastLoopDate:
         case .lastLoopDate:
-            return NSLocalizedString("Last Loop Time", comment: "")
+            return String(localized: "Last Loop Time", comment: "")
         case .cob:
         case .cob:
-            return NSLocalizedString("COB", comment: "")
+            return String(localized: "COB", comment: "")
         case .iob:
         case .iob:
-            return NSLocalizedString("IOB", comment: "")
+            return String(localized: "IOB", comment: "")
         case .ring:
         case .ring:
-            return NSLocalizedString("Loop Status", comment: "")
+            return String(localized: "Loop Status", comment: "")
         }
         }
     }
     }
 }
 }
@@ -159,9 +159,9 @@ enum ContactImageLayout: String, JSON, CaseIterable, Identifiable, Codable {
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
         case .default:
         case .default:
-            return NSLocalizedString("Default", comment: "")
+            return String(localized: "Default", comment: "")
         case .split:
         case .split:
-            return NSLocalizedString("Split", comment: "")
+            return String(localized: "Split", comment: "")
         }
         }
     }
     }
 }
 }
@@ -178,15 +178,15 @@ enum ContactImageLargeRing: String, JSON, CaseIterable, Identifiable, Codable {
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
         case .none:
         case .none:
-            return NSLocalizedString("Hidden", comment: "")
+            return String(localized: "Hidden", comment: "")
         case .loop:
         case .loop:
-            return NSLocalizedString("Loop Status", comment: "")
+            return String(localized: "Loop Status", comment: "")
 //        case .iob:
 //        case .iob:
-//            return NSLocalizedString("Insulin on Board (IOB)", comment: "")
+//            return String(localized: "Insulin on Board (IOB)", comment: "")
 //        case .cob:
 //        case .cob:
-//            return NSLocalizedString("Carbs on Board (COB)", comment: "")
+//            return String(localized: "Carbs on Board (COB)", comment: "")
 //        case .iobcob:
 //        case .iobcob:
-//            return NSLocalizedString("IOB + COB", comment: "")
+//            return String(localized: "IOB + COB", comment: "")
         }
         }
     }
     }
 }
 }

+ 0 - 0
Trio/Sources/Models/ForecastDisplayType.swift


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است