فهرست منبع

shift work off from the main thread by observing saves in the managedObjectContexts, let battery only update instead of always saving a new battery object to Core Data, prevent bolus progress bar from showing external boli

polscm32 1 سال پیش
والد
کامیت
40a0b722bb

+ 24 - 14
FreeAPS/Sources/APS/APSManager.swift

@@ -1458,28 +1458,38 @@ private extension PumpManager {
 extension BaseAPSManager: PumpManagerStatusObserver {
     func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
         let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
-        let battery = Battery(
-            percent: percent,
-            voltage: nil,
-            string: percent > 10 ? .normal : .low,
-            display: status.pumpBatteryChargeRemaining != nil
-        )
 
         privateContext.perform {
-            let batteryToStore = OpenAPS_Battery(context: self.privateContext)
-            batteryToStore.id = UUID()
-            batteryToStore.date = Date()
-            batteryToStore.percent = Int16(percent)
-            batteryToStore.voltage = nil
-            batteryToStore.status = percent > 10 ? "normal" : "low"
-            batteryToStore.display = status.pumpBatteryChargeRemaining != nil
+            /// only update the last item with the current battery infos instead of saving a new one each time
+            let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
+            fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
+            fetchRequest.predicate = NSPredicate.predicateFor30MinAgo
+            fetchRequest.fetchLimit = 1
+
             do {
+                let results = try self.privateContext.fetch(fetchRequest)
+                let batteryToStore: OpenAPS_Battery
+
+                if let existingBattery = results.first {
+                    batteryToStore = existingBattery
+                } else {
+                    batteryToStore = OpenAPS_Battery(context: self.privateContext)
+                    batteryToStore.id = UUID()
+                }
+
+                batteryToStore.date = Date()
+                batteryToStore.percent = Int16(percent)
+                batteryToStore.voltage = nil
+                batteryToStore.status = percent > 10 ? "normal" : "low"
+                batteryToStore.display = status.pumpBatteryChargeRemaining != nil
+
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
             } catch {
-                print(error.localizedDescription)
+                print("Failed to fetch or save battery: \(error.localizedDescription)")
             }
         }
+        // TODO: - remove this after ensuring that NS still gets the same infos from Core Data
         storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
     }
 }

+ 137 - 89
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -103,8 +103,8 @@ extension Bolus {
 
         override func subscribe() {
             setupNotification()
-            updateGlucose()
-            updateDetermination()
+            setupGlucoseArray()
+            setupDeterminationsArray()
 
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(BolusFailureObserver.self, observer: self)
@@ -177,93 +177,6 @@ extension Bolus {
             }
         }
 
-        // MARK: - Setup Notifications
-
-        /// listens for the notifications sent when the managedObjectContext has changed
-        func setupNotification() {
-            Foundation.NotificationCenter.default.addObserver(
-                self,
-                selector: #selector(contextDidSave(_:)),
-                name: Notification.Name.NSManagedObjectContextObjectsDidChange,
-                object: backgroundContext
-            )
-        }
-
-        /// determine the actions when the context has changed
-        ///
-        /// its done on a background thread and after that the UI gets updated on the main thread
-        @objc private func contextDidSave(_ notification: Notification) {
-            guard let userInfo = notification.userInfo else { return }
-
-            Task { [weak self] in
-                await self?.processUpdates(userInfo: userInfo)
-            }
-        }
-
-        private func processUpdates(userInfo: [AnyHashable: Any]) async {
-            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
-
-            let glucoseUpdates = objects.filter { $0 is GlucoseStored }
-            let determinationUpdates = objects.filter { $0 is OrefDetermination }
-
-            if glucoseUpdates.isNotEmpty {
-                updateGlucose()
-            }
-            if determinationUpdates.isNotEmpty {
-                updateDetermination()
-            }
-        }
-
-        // MARK: - Glucose
-
-        private func updateGlucose() {
-            CoreDataStack.shared.fetchEntitiesAndUpdateUI(
-                ofType: GlucoseStored.self,
-                predicate: NSPredicate.predicateFor30MinAgo,
-                key: "date",
-                ascending: false,
-                fetchLimit: 3
-            ) { fetchedValues in
-                self.glucoseFromPersistence = fetchedValues
-
-                let lastGlucose = self.glucoseFromPersistence.first?.glucose ?? 0
-                let thirdLastGlucose = self.glucoseFromPersistence.last?.glucose ?? 0
-                let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
-
-                self.currentBG = Decimal(lastGlucose)
-                self.deltaBG = delta
-            }
-        }
-
-        private func updateDetermination() {
-            CoreDataStack.shared.fetchEntitiesAndUpdateUI(
-                ofType: OrefDetermination.self,
-                predicate: NSPredicate.enactedDetermination,
-                key: "deliverAt",
-                ascending: false,
-                fetchLimit: 1
-            ) { fetchedValues in
-                guard let mostRecentDetermination = fetchedValues.first else { return }
-                self.determination = fetchedValues
-
-                // setup vars for bolus calculation
-                self.insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
-                self.evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
-                self.insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
-                self.target = (mostRecentDetermination.currentTarget ?? 100) as Decimal
-                self.isf = (mostRecentDetermination.insulinSensitivity ?? 0) as Decimal
-                self.cob = mostRecentDetermination.cob as Int16
-                self.iob = (mostRecentDetermination.iob ?? 0) as Decimal
-                self.basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
-                self.carbRatio = (mostRecentDetermination.carbRatio ?? 0) as Decimal
-
-                self.getCurrentBasal()
-                self.insulinCalculated = self.calculateInsulin()
-            }
-        }
-
         // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
 
         /// Calculate insulin recommendation
@@ -653,3 +566,138 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
         }
     }
 }
+
+// MARK: - Setup Notifications
+
+extension Bolus.StateModel {
+
+    /// listens for the notifications sent when the managedObjectContext has saved!
+    func setupNotification() {
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(contextDidSave(_:)),
+            name: Notification.Name.NSManagedObjectContextDidSave,
+            object: backgroundContext
+        )
+    }
+
+    /// determine the actions when the context has changed
+    ///
+    /// its done on a background thread and after that the UI gets updated on the main thread
+    @objc private func contextDidSave(_ notification: Notification) {
+        guard let userInfo = notification.userInfo else { return }
+
+        Task { [weak self] in
+            await self?.processUpdates(userInfo: userInfo)
+        }
+    }
+
+    private func processUpdates(userInfo: [AnyHashable: Any]) async {
+        var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
+        objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
+        objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+
+        let glucoseUpdates = objects.filter { $0 is GlucoseStored }
+        let determinationUpdates = objects.filter { $0 is OrefDetermination }
+
+        DispatchQueue.global(qos: .background).async {
+            if glucoseUpdates.isNotEmpty {
+                self.setupGlucoseArray()
+            }
+            if determinationUpdates.isNotEmpty {
+                self.setupDeterminationsArray()
+            }
+        }
+    }
+}
+
+// MARK: - Setup Glucose and Determinations
+
+extension Bolus.StateModel {
+    
+    // Glucose
+    private func setupGlucoseArray() {
+        Task {
+            let ids = await self.fetchGlucose()
+            await updateGlucoseArray(with: ids)
+        }
+    }
+
+    private func fetchGlucose() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 3
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let glucoseObjects = try IDs.compactMap { id in
+                try context.existingObject(with: id) as? GlucoseStored
+            }
+            glucoseFromPersistence = glucoseObjects
+
+            let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
+            let thirdLastGlucose = glucoseFromPersistence.last?.glucose ?? 0
+            let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+
+            currentBG = Decimal(lastGlucose)
+            deltaBG = delta
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Determinations
+    private func setupDeterminationsArray() {
+        Task {
+            let ids = await self.fetchDeterminations()
+            await updateDeterminationsArray(with: ids)
+        }
+    }
+
+    private func fetchDeterminations() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate.enactedDetermination,
+            key: "deliverAt",
+            ascending: false,
+            fetchLimit: 1
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateDeterminationsArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let determinationObjects = try IDs.compactMap { id in
+                try context.existingObject(with: id) as? OrefDetermination
+            }
+            guard let mostRecentDetermination = determinationObjects.first else { return }
+            determination = determinationObjects
+
+            // setup vars for bolus calculation
+            insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
+            evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
+            insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
+            target = (mostRecentDetermination.currentTarget ?? 100) as Decimal
+            isf = (mostRecentDetermination.insulinSensitivity ?? 0) as Decimal
+            cob = mostRecentDetermination.cob as Int16
+            iob = (mostRecentDetermination.iob ?? 0) as Decimal
+            basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
+            carbRatio = (mostRecentDetermination.carbRatio ?? 0) as Decimal
+
+            getCurrentBasal()
+            insulinCalculated = calculateInsulin()
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the determinations array: \(error.localizedDescription)"
+            )
+        }
+    }
+}

+ 332 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -68,9 +68,28 @@ extension Home {
 
         @Published var waitForSuggestion: Bool = false
 
+        @Published var glucoseFromPersistence: [GlucoseStored] = []
+        @Published var manualGlucoseFromPersistence: [GlucoseStored] = []
+        @Published var carbsFromPersistence: [CarbEntryStored] = []
+        @Published var fpusFromPersistence: [CarbEntryStored] = []
+        @Published var determinationsFromPersistence: [OrefDetermination] = []
+        @Published var insulinFromPersistence: [PumpEventStored] = []
+        @Published var batteryFromPersistence: [OpenAPS_Battery] = []
+        @Published var lastPumpBolus: PumpEventStored?
+
         let context = CoreDataStack.shared.newTaskContext()
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
+            setupNotification()
+            setupGlucoseArray()
+            setupManualGlucoseArray()
+            setupCarbsArray()
+            setupFPUsArray()
+            setupDeterminationsArray()
+            setupInsulinArray()
+            setupLastBolus()
+            setupBatteryArray()
             setupBasals()
             setupBoluses()
             setupSuspensions()
@@ -443,3 +462,316 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // TODO:
     }
 }
+
+// MARK: - Setup Core Data observation
+
+extension Home.StateModel {
+    /// listens for the notifications sent when the managedObjectContext has saved!
+    func setupNotification() {
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(contextDidSave(_:)),
+            name: Notification.Name.NSManagedObjectContextDidSave,
+            object: nil
+        )
+    }
+
+    /// determine the actions when the context has changed
+    ///
+    /// its done on a background thread and after that the UI gets updated on the main thread
+    @objc private func contextDidSave(_ notification: Notification) {
+        guard let userInfo = notification.userInfo else { return }
+
+        Task { [weak self] in
+            await self?.processUpdates(userInfo: userInfo)
+        }
+    }
+
+    private func processUpdates(userInfo: [AnyHashable: Any]) async {
+        var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
+        objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
+        objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+
+        let glucoseUpdates = objects.filter { $0 is GlucoseStored }
+        let determinationUpdates = objects.filter { $0 is OrefDetermination }
+        let carbUpdates = objects.filter { $0 is CarbEntryStored }
+        let insulinUpdates = objects.filter { $0 is PumpEventStored }
+        let batteryUpdates = objects.filter { $0 is OpenAPS_Battery }
+
+        DispatchQueue.global(qos: .background).async {
+            if !glucoseUpdates.isEmpty {
+                self.setupGlucoseArray()
+                self.setupManualGlucoseArray()
+            }
+            if !determinationUpdates.isEmpty {
+                self.setupDeterminationsArray()
+            }
+            if !carbUpdates.isEmpty {
+                self.setupCarbsArray()
+                self.setupFPUsArray()
+            }
+            if !insulinUpdates.isEmpty {
+                self.setupInsulinArray()
+                self.setupLastBolus()
+            }
+            if !batteryUpdates.isEmpty {
+                self.setupBatteryArray()
+            }
+        }
+    }
+}
+
+// MARK: - Handle Core Data changes and update Arrays to display them in the UI
+
+extension Home.StateModel {
+    
+    // Setup Glucose
+    private func setupGlucoseArray() {
+        Task {
+            let ids = await self.fetchGlucose()
+            await updateGlucoseArray(with: ids)
+        }
+    }
+
+    private func fetchGlucose() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.glucose,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let glucoseObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? GlucoseStored
+            }
+            glucoseFromPersistence = glucoseObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup Manual Glucose
+    private func setupManualGlucoseArray() {
+        Task {
+            let ids = await self.fetchGlucose()
+            await updateGlucoseArray(with: ids)
+        }
+    }
+
+    private func fetchManualGlucose() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.manualGlucose,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateManualGlucoseArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let manualGlucoseObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? GlucoseStored
+            }
+            manualGlucoseFromPersistence = manualGlucoseObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the manual glucose array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup Carbs
+    private func setupCarbsArray() {
+        Task {
+            let ids = await self.fetchCarbs()
+            await updateCarbsArray(with: ids)
+        }
+    }
+
+    private func fetchCarbs() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: NSPredicate.carbsForChart,
+            key: "date",
+            ascending: false
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateCarbsArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let carbObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? CarbEntryStored
+            }
+            carbsFromPersistence = carbObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the carbs array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup FPUs
+    private func setupFPUsArray() {
+        Task {
+            let ids = await self.fetchFPUs()
+            await updateFPUsArray(with: ids)
+        }
+    }
+
+    private func fetchFPUs() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: NSPredicate.fpusForChart,
+            key: "date",
+            ascending: false
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateFPUsArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let fpuObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? CarbEntryStored
+            }
+            fpusFromPersistence = fpuObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the fpus array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup Determinations
+    private func setupDeterminationsArray() {
+        Task {
+            let ids = await self.fetchDeterminations()
+            await updateDeterminationsArray(with: ids)
+        }
+    }
+
+    private func fetchDeterminations() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate.enactedDetermination,
+            key: "deliverAt",
+            ascending: false,
+            fetchLimit: 1
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateDeterminationsArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let determinationObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OrefDetermination
+            }
+            determinationsFromPersistence = determinationObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed)  error while updating the determinations array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup Insulin
+    private func setupInsulinArray() {
+        Task {
+            let ids = await self.fetchInsulin()
+            await updateInsulinArray(with: ids)
+        }
+    }
+
+    private func fetchInsulin() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpHistoryLast24h,
+            key: "timestamp",
+            ascending: true
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateInsulinArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let insulinObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? PumpEventStored
+            }
+            insulinFromPersistence = insulinObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup Last Bolus to display the bolus progress bar
+    // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
+    private func setupLastBolus() {
+        Task {
+            guard let id = await self.fetchLastBolus() else { return }
+            await updateLastBolus(with: id)
+        }
+    }
+
+    private func fetchLastBolus() async -> NSManagedObjectID? {
+        CoreDataStack.shared.fetchEntities(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastPumpBolus,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1
+        ).map(\.objectID).first
+    }
+
+    @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
+        do {
+            lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // Setup Battery
+    private func setupBatteryArray() {
+        Task {
+            let ids = await self.fetchBattery()
+            await updateBatteryArray(with: ids)
+        }
+    }
+
+    private func fetchBattery() async -> [NSManagedObjectID] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: OpenAPS_Battery.self,
+            onContext: context,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false
+        ).map(\.objectID)
+    }
+
+    @MainActor private func updateBatteryArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let batteryObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OpenAPS_Battery
+            }
+            batteryFromPersistence = batteryObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the battery array: \(error.localizedDescription)"
+            )
+        }
+    }
+}

+ 17 - 49
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -88,38 +88,6 @@ struct MainChartView: View {
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.calendar) var calendar
 
-    // MARK: - Core Data Fetch Requests
-
-    @FetchRequest(
-        fetchRequest: PumpEventStored.fetch(NSPredicate.pumpHistoryLast24h, ascending: true),
-        animation: Animation.bouncy
-    ) var insulinFromPersistence: FetchedResults<PumpEventStored>
-
-    @FetchRequest(
-        fetchRequest: GlucoseStored.fetch(NSPredicate.manualGlucose, ascending: true),
-        animation: Animation.bouncy
-    ) var manualGlucoseFromPersistence: FetchedResults<GlucoseStored>
-
-    @FetchRequest(
-        fetchRequest: GlucoseStored.fetch(NSPredicate.glucose, ascending: true),
-        animation: Animation.bouncy
-    ) var glucoseFromPersistence: FetchedResults<GlucoseStored>
-
-    @FetchRequest(
-        fetchRequest: CarbEntryStored.fetch(NSPredicate.carbsForChart, ascending: true),
-        animation: Animation.bouncy
-    ) var carbsFromPersistence: FetchedResults<CarbEntryStored>
-
-    @FetchRequest(
-        fetchRequest: CarbEntryStored.fetch(NSPredicate.fpusForChart, ascending: true),
-        animation: Animation.bouncy
-    ) var fpusFromPersistence: FetchedResults<CarbEntryStored>
-
-    @FetchRequest(
-        fetchRequest: OrefDetermination.fetch(NSPredicate.predicateFor30MinAgoForDetermination),
-        animation: .bouncy
-    ) var determination: FetchedResults<OrefDetermination>
-
     private var bolusFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
@@ -156,7 +124,7 @@ struct MainChartView: View {
         if let selection = selection {
             let lowerBound = selection.addingTimeInterval(-120)
             let upperBound = selection.addingTimeInterval(120)
-            return glucoseFromPersistence.first { $0.date ?? now >= lowerBound && $0.date ?? now <= upperBound }
+            return state.glucoseFromPersistence.first { $0.date ?? now >= lowerBound && $0.date ?? now <= upperBound }
         } else {
             return nil
         }
@@ -174,12 +142,12 @@ struct MainChartView: View {
                         yAxisChartData()
                         scroller.scrollTo("MainChart", anchor: .trailing)
                     }
-                    .onChange(of: glucoseFromPersistence.last?.glucose) { _ in
+                    .onChange(of: state.glucoseFromPersistence.last?.glucose) { _ in
                         updateStartEndMarkers()
                         yAxisChartData()
                         scroller.scrollTo("MainChart", anchor: .trailing)
                     }
-                    .onChange(of: determination.last?.deliverAt) { _ in
+                    .onChange(of: state.determinationsFromPersistence.last?.deliverAt) { _ in
                         updateStartEndMarkers()
                         scroller.scrollTo("MainChart", anchor: .trailing)
                     }
@@ -363,7 +331,7 @@ extension MainChartView {
 extension MainChartView {
     private func drawBoluses() -> some ChartContent {
         /// smbs in triangle form
-        ForEach(insulinFromPersistence) { insulin in
+        ForEach(state.insulinFromPersistence) { insulin in
             let amount = insulin.bolus?.amount ?? 0 as NSDecimalNumber
             let bolusDate = insulin.timestamp ?? Date()
             let glucose = timeToNearestGlucose(time: bolusDate.timeIntervalSince1970)?.glucose ?? 120
@@ -390,7 +358,7 @@ extension MainChartView {
 
     private func drawCarbs() -> some ChartContent {
         /// carbs
-        ForEach(carbsFromPersistence) { carb in
+        ForEach(state.carbsFromPersistence) { carb in
             let carbAmount = carb.carbs
             let yPosition = units == .mgdL ? 60 : 3.33
 
@@ -409,7 +377,7 @@ extension MainChartView {
 
     private func drawFpus() -> some ChartContent {
         /// fpus
-        ForEach(fpusFromPersistence, id: \.id) { fpu in
+        ForEach(state.fpusFromPersistence, id: \.id) { fpu in
             let fpuAmount = fpu.carbs
             let size = (Config.fpuSize + CGFloat(fpuAmount) * Config.carbsScale) * 1.8
             let yPosition = units == .mgdL ? 60 : 3.33
@@ -426,7 +394,7 @@ extension MainChartView {
     private func drawGlucose() -> some ChartContent {
         /// glucose point mark
         /// filtering for high and low bounds in settings
-        ForEach(glucoseFromPersistence) { item in
+        ForEach(state.glucoseFromPersistence) { item in
             if smooth {
                 if item.glucose > Int(highGlucose) {
                     PointMark(
@@ -521,7 +489,7 @@ extension MainChartView {
     }
 
     private func preprocessForecastData() -> [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] {
-        determination
+        state.determinationsFromPersistence
             .compactMap { determination -> NSManagedObjectID? in
                 determination.objectID
             }
@@ -582,7 +550,7 @@ extension MainChartView {
 
     private func drawManualGlucose() -> some ChartContent {
         /// manual glucose mark
-        ForEach(manualGlucoseFromPersistence) { item in
+        ForEach(state.manualGlucoseFromPersistence) { item in
             let manualGlucose = item.glucose
             PointMark(
                 x: .value("Time", item.date ?? Date(), unit: .second),
@@ -624,7 +592,7 @@ extension MainChartView {
 
     private func prepareTempBasals() -> [(start: Date, end: Date, rate: Double)] {
         let now = Date()
-        return insulinFromPersistence.compactMap { temp -> (start: Date, end: Date, rate: Double)? in
+        return state.insulinFromPersistence.compactMap { temp -> (start: Date, end: Date, rate: Double)? in
             let duration = temp.tempBasal?.duration ?? 0
             let timestamp = temp.timestamp ?? Date()
             let end = min(timestamp + duration.minutes, now)
@@ -632,7 +600,7 @@ extension MainChartView {
             let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
 
             // Check if there's a subsequent temp basal to determine the end time
-            guard let nextTemp = insulinFromPersistence.first(where: { $0.timestamp ?? .distantPast > timestamp }) else {
+            guard let nextTemp = state.insulinFromPersistence.first(where: { $0.timestamp ?? .distantPast > timestamp }) else {
                 return (timestamp, end, rate)
             }
             return (timestamp, nextTemp.timestamp ?? Date(), rate) // end defaults to current time
@@ -674,21 +642,21 @@ extension MainChartView {
 
     /// calculates the glucose value thats the nearest to parameter 'time'
     private func timeToNearestGlucose(time: TimeInterval) -> GlucoseStored? {
-        guard !glucoseFromPersistence.isEmpty else {
+        guard !state.glucoseFromPersistence.isEmpty else {
             return nil
         }
 
         var low = 0
-        var high = glucoseFromPersistence.count - 1
+        var high = state.glucoseFromPersistence.count - 1
         var closestGlucose: GlucoseStored?
 
         // binary search to find next glucose
         while low <= high {
             let mid = low + (high - low) / 2
-            let midTime = glucoseFromPersistence[mid].date?.timeIntervalSince1970 ?? 0
+            let midTime = state.glucoseFromPersistence[mid].date?.timeIntervalSince1970 ?? 0
 
             if midTime == time {
-                return glucoseFromPersistence[mid]
+                return state.glucoseFromPersistence[mid]
             } else if midTime < time {
                 low = mid + 1
             } else {
@@ -697,7 +665,7 @@ extension MainChartView {
 
             // update if necessary
             if closestGlucose == nil || abs(midTime - time) < abs(closestGlucose!.date?.timeIntervalSince1970 ?? 0 - time) {
-                closestGlucose = glucoseFromPersistence[mid]
+                closestGlucose = state.glucoseFromPersistence[mid]
             }
         }
 
@@ -896,7 +864,7 @@ extension MainChartView {
     // MARK: - Chart formatting
 
     private func yAxisChartData() {
-        let glucoseMapped = glucoseFromPersistence.map(\.glucose)
+        let glucoseMapped = state.glucoseFromPersistence.map(\.glucose)
         guard let minGlucose = glucoseMapped.min(), let maxGlucose = glucoseMapped.max() else {
             // default values
             minValue = 45 * conversionFactor - 20 * conversionFactor

+ 8 - 15
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -7,7 +7,7 @@ struct CurrentGlucoseView: View {
     @Binding var alarm: GlucoseAlarm?
     @Binding var lowGlucose: Decimal
     @Binding var highGlucose: Decimal
-//    var glucoseFromPersistence: [GlucoseStored]
+    var latestGlucoseValues: [GlucoseStored]
 
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [
@@ -21,13 +21,6 @@ struct CurrentGlucoseView: View {
 
     @Environment(\.colorScheme) var colorScheme
 
-    @FetchRequest(
-        entity: GlucoseStored.entity(),
-        sortDescriptors: [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)],
-        predicate: NSPredicate.predicateFor30MinAgo,
-        animation: Animation.bouncy
-    ) var glucoseFromPersistence: FetchedResults<GlucoseStored>
-
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
@@ -72,7 +65,7 @@ struct CurrentGlucoseView: View {
 
             VStack(alignment: .center) {
                 HStack {
-                    let glucoseValue = glucoseFromPersistence.first?.glucose ?? 100
+                    let glucoseValue = latestGlucoseValues.first?.glucose ?? 100
                     let displayGlucose = convertGlucose(glucoseValue, to: units)
 
                     Text(
@@ -83,7 +76,7 @@ struct CurrentGlucoseView: View {
                     .foregroundColor(alarm == nil ? colourGlucoseText : .loopRed)
                 }
                 HStack {
-                    let minutesAgo = -1 * (glucoseFromPersistence.first?.date?.timeIntervalSinceNow ?? 0) / 60
+                    let minutesAgo = -1 * (latestGlucoseValues.first?.date?.timeIntervalSinceNow ?? 0) / 60
                     let text = timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
                     Text(
                         minutesAgo <= 1 ? "< 1 " + NSLocalizedString("min", comment: "Short form for minutes") : (
@@ -100,7 +93,7 @@ struct CurrentGlucoseView: View {
                 }.frame(alignment: .top)
             }
         }
-        .onChange(of: glucoseFromPersistence.first?.direction) { newDirection in
+        .onChange(of: latestGlucoseValues.first?.direction) { newDirection in
             withAnimation {
                 switch newDirection {
                 case "DoubleUp",
@@ -138,12 +131,12 @@ struct CurrentGlucoseView: View {
     }
 
     private var delta: String {
-        guard glucoseFromPersistence.count >= 2 else {
+        guard latestGlucoseValues.count >= 2 else {
             return "--"
         }
 
-        let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
-        let secondLastGlucose = glucoseFromPersistence.dropFirst().first?.glucose ?? 0
+        let lastGlucose = latestGlucoseValues.first?.glucose ?? 0
+        let secondLastGlucose = latestGlucoseValues.dropFirst().first?.glucose ?? 0
         let delta = lastGlucose - secondLastGlucose
         let deltaAsDecimal = Decimal(delta)
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
@@ -151,7 +144,7 @@ struct CurrentGlucoseView: View {
 
     var colourGlucoseText: Color {
         // Fetch the first glucose reading and convert it to Int for comparison
-        let whichGlucose = Int(glucoseFromPersistence.first?.glucose ?? 0)
+        let whichGlucose = Int(latestGlucoseValues.first?.glucose ?? 0)
 
         // Define default color based on the color scheme
         let defaultColor: Color = colorScheme == .dark ? .white : .black

+ 1 - 4
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -14,10 +14,7 @@ struct LoopView: View {
     @Binding var lastLoopDate: Date
     @Binding var manualTempBasal: Bool
 
-    @FetchRequest(
-        fetchRequest: OrefDetermination.fetch(NSPredicate.enactedDetermination),
-        animation: .bouncy
-    ) var determination: FetchedResults<OrefDetermination>
+    var determination: [OrefDetermination]
 
     private var dateFormatter: DateFormatter {
         let formatter = DateFormatter()

+ 1 - 8
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -3,18 +3,11 @@ import SwiftUI
 
 struct PumpView: View {
     @Binding var reservoir: Decimal?
-//    @Binding var battery: Battery?
     @Binding var name: String
     @Binding var expiresAtDate: Date?
     @Binding var timerDate: Date
     @Binding var timeZone: TimeZone?
-
-    @State var state: Home.StateModel
-
-    @FetchRequest(
-        fetchRequest: OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo),
-        animation: Animation.bouncy
-    ) var battery: FetchedResults<OpenAPS_Battery>
+    var battery: [OpenAPS_Battery]
 
     @Environment(\.colorScheme) var colorScheme
 

+ 69 - 72
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -43,16 +43,6 @@ extension Home {
         ) var fetchedPercent: FetchedResults<Override>
 
         @FetchRequest(
-            fetchRequest: OrefDetermination.fetch(NSPredicate.enactedDetermination),
-            animation: .bouncy
-        ) var determination: FetchedResults<OrefDetermination>
-
-        @FetchRequest(
-            fetchRequest: PumpEventStored.fetch(NSPredicate.recentPumpHistory, ascending: false, fetchLimit: 1),
-            animation: .bouncy
-        ) var recentPumpHistory: FetchedResults<PumpEventStored>
-
-        @FetchRequest(
             entity: OverridePresets.entity(),
             sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(
                 format: "name != %@", "" as String
@@ -154,7 +144,8 @@ extension Home {
                 units: $state.units,
                 alarm: $state.alarm,
                 lowGlucose: $state.lowGlucose,
-                highGlucose: $state.highGlucose
+                highGlucose: $state.highGlucose,
+                latestGlucoseValues: state.glucoseFromPersistence
             ).scaleEffect(0.9)
                 .onTapGesture {
                     if state.alarm == nil {
@@ -180,8 +171,7 @@ extension Home {
                 name: $state.pumpName,
                 expiresAtDate: $state.pumpExpiresAtDate,
                 timerDate: $state.timerDate,
-                timeZone: $state.timeZone,
-                state: state
+                timeZone: $state.timeZone, battery: state.batteryFromPersistence
             ).onTapGesture {
                 if state.pumpDisplayState != nil {
                     state.setupPump = true
@@ -190,7 +180,7 @@ extension Home {
         }
 
         var tempBasalString: String? {
-            guard let tempRate = recentPumpHistory.first?.tempBasal?.rate else {
+            guard let tempRate = state.insulinFromPersistence.last?.tempBasal?.rate else {
                 return nil
             }
             let rateString = numberFormatter.string(from: tempRate as NSNumber) ?? "0"
@@ -427,7 +417,8 @@ extension Home {
                     timerDate: $state.timerDate,
                     isLooping: $state.isLooping,
                     lastLoopDate: $state.lastLoopDate,
-                    manualTempBasal: $state.manualTempBasal
+                    manualTempBasal: $state.manualTempBasal,
+                    determination: state.determinationsFromPersistence
                 ).onTapGesture {
                     state.isStatusPopupPresented = true
                     setStatusTitle()
@@ -438,7 +429,7 @@ extension Home {
                 }
                 /// eventualBG string at bottomTrailing
 
-                if let eventualBG = determination.first?.eventualBG {
+                if let eventualBG = state.determinationsFromPersistence.first?.eventualBG {
                     let bg = eventualBG as Decimal
                     HStack {
                         Image(systemName: "arrow.right.circle")
@@ -486,7 +477,7 @@ extension Home {
                         .font(.system(size: 16))
                         .foregroundColor(Color.insulin)
                     Text(
-                        (numberFormatter.string(from: (determination.first?.iob ?? 0) as NSNumber) ?? "0") +
+                        (numberFormatter.string(from: (state.determinationsFromPersistence.first?.iob ?? 0) as NSNumber) ?? "0") +
                             NSLocalizedString(" U", comment: "Insulin unit")
                     )
                     .font(.system(size: 16, weight: .bold, design: .rounded))
@@ -499,7 +490,7 @@ extension Home {
                         .font(.system(size: 16))
                         .foregroundColor(.loopYellow)
                     Text(
-                        (numberFormatter.string(from: (determination.first?.cob ?? 0) as NSNumber) ?? "0") +
+                        (numberFormatter.string(from: (state.determinationsFromPersistence.first?.cob ?? 0) as NSNumber) ?? "0") +
                             NSLocalizedString(" g", comment: "gram of carbs")
                     )
                     .font(.system(size: 16, weight: .bold, design: .rounded))
@@ -525,7 +516,8 @@ extension Home {
                         "TDD: " +
                             (
                                 numberFormatter
-                                    .string(from: (determination.first?.totalDailyDose ?? 0) as NSNumber) ?? "0"
+                                    .string(from: (state.determinationsFromPersistence.first?.totalDailyDose ?? 0) as NSNumber) ??
+                                    "0"
                             ) +
                             NSLocalizedString(" U", comment: "Insulin unit")
                     )
@@ -677,62 +669,66 @@ extension Home {
         }
 
         @ViewBuilder func bolusView(_: GeometryProxy, _ progress: Decimal) -> some View {
-            let bolusTotal = (recentPumpHistory.first?.bolus?.amount) as? Decimal ?? 0
-            let bolusFraction = progress * bolusTotal
-
-            let bolusString =
-                (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
-                    + " of " +
-                    (numberFormatter.string(from: bolusTotal as NSNumber) ?? "0")
-                    + NSLocalizedString(" U", comment: "Insulin unit")
-
-            ZStack {
-                /// rectangle as background
-                RoundedRectangle(cornerRadius: 15)
-                    .fill(
-                        colorScheme == .dark ? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745) : Color.insulin
-                            .opacity(0.2)
-                    )
-                    .clipShape(RoundedRectangle(cornerRadius: 15))
-                    .frame(height: UIScreen.main.bounds.height / 18)
-                    .shadow(
-                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
-                            Color.black.opacity(0.33),
-                        radius: 3
-                    )
+            /// ensure that state.lastPumpBolus has a value, i.e. there is a last bolus done by the pump and not an external bolus
+            /// - TRUE:  show the pump bolus
+            /// - FALSE:  do not show a progress bar at all
+            if let bolusTotal = state.lastPumpBolus?.bolus?.amount {
+                let bolusFraction = progress * (bolusTotal as Decimal)
+                let bolusString =
+                    (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
+                        + " of " +
+                        (numberFormatter.string(from: bolusTotal as NSNumber) ?? "0")
+                        + NSLocalizedString(" U", comment: "Insulin unit")
+
+                ZStack {
+                    /// rectangle as background
+                    RoundedRectangle(cornerRadius: 15)
+                        .fill(
+                            colorScheme == .dark ? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745) : Color
+                                .insulin
+                                .opacity(0.2)
+                        )
+                        .clipShape(RoundedRectangle(cornerRadius: 15))
+                        .frame(height: UIScreen.main.bounds.height / 18)
+                        .shadow(
+                            color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
+                                Color.black.opacity(0.33),
+                            radius: 3
+                        )
 
-                /// actual bolus view
-                HStack {
-                    Image(systemName: "cross.vial.fill")
-                        .font(.system(size: 25))
+                    /// actual bolus view
+                    HStack {
+                        Image(systemName: "cross.vial.fill")
+                            .font(.system(size: 25))
 
-                    Spacer()
+                        Spacer()
 
-                    VStack {
-                        Text("Bolusing")
-                            .font(.subheadline)
-                            .frame(maxWidth: .infinity, alignment: .leading)
-                        Text(bolusString)
-                            .font(.caption)
-                            .frame(maxWidth: .infinity, alignment: .leading)
-                    }.padding(.leading, 5)
+                        VStack {
+                            Text("Bolusing")
+                                .font(.subheadline)
+                                .frame(maxWidth: .infinity, alignment: .leading)
+                            Text(bolusString)
+                                .font(.caption)
+                                .frame(maxWidth: .infinity, alignment: .leading)
+                        }.padding(.leading, 5)
 
-                    Spacer()
+                        Spacer()
 
-                    Button {
-                        state.waitForSuggestion = true
-                        state.cancelBolus()
-                    } label: {
-                        Image(systemName: "xmark.app")
-                            .font(.system(size: 25))
-                    }
-                }.padding(.horizontal, 10)
-                    .padding(.trailing, 8)
+                        Button {
+                            state.waitForSuggestion = true
+                            state.cancelBolus()
+                        } label: {
+                            Image(systemName: "xmark.app")
+                                .font(.system(size: 25))
+                        }
+                    }.padding(.horizontal, 10)
+                        .padding(.trailing, 8)
 
-            }.padding(.horizontal, 10).padding(.bottom, 10)
-                .overlay(alignment: .bottom) {
-                    bolusProgressBar(progress).padding(.horizontal, 18).offset(y: 45)
-                }.clipShape(RoundedRectangle(cornerRadius: 15))
+                }.padding(.horizontal, 10).padding(.bottom, 10)
+                    .overlay(alignment: .bottom) {
+                        bolusProgressBar(progress).padding(.horizontal, 18).offset(y: 45)
+                    }.clipShape(RoundedRectangle(cornerRadius: 15))
+            }
         }
 
         @ViewBuilder func mainView() -> some View {
@@ -810,7 +806,8 @@ extension Home {
             ZStack(alignment: .bottom) {
                 TabView {
                     let carbsRequiredBadge: String? = {
-                        guard let carbsRequired = determination.first?.carbsRequired as? Decimal else { return nil }
+                        guard let carbsRequired = state.determinationsFromPersistence.first?.carbsRequired as? Decimal
+                        else { return nil }
                         if carbsRequired > state.settingsManager.settings.carbsRequiredThreshold {
                             let numberAsNSNumber = NSDecimalNumber(decimal: carbsRequired)
                             let formattedNumber = numberFormatter.string(from: numberAsNSNumber) ?? ""
@@ -873,7 +870,7 @@ extension Home {
             VStack(alignment: .leading, spacing: 4) {
                 Text(statusTitle).font(.headline).foregroundColor(.white)
                     .padding(.bottom, 4)
-                if let determination = determination.first {
+                if let determination = state.determinationsFromPersistence.first {
                     if determination.glucose == 400 {
                         Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed).padding(.top, 8)
                         Text("SMBs and High Temps Disabled.").font(.caption).foregroundColor(.white).padding(.bottom, 4)
@@ -898,7 +895,7 @@ extension Home {
         }
 
         private func setStatusTitle() {
-            if let determination = determination.first {
+            if let determination = state.determinationsFromPersistence.first {
                 let dateFormatter = DateFormatter()
                 dateFormatter.timeStyle = .short
                 statusTitle = NSLocalizedString("Oref Determination enacted at", comment: "Headline in enacted pop up") +

+ 5 - 0
Model/Helper/PumpEvent+helper.swift

@@ -61,6 +61,11 @@ extension NSPredicate {
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
     }
 
+    static var lastPumpBolus: NSPredicate {
+        let date = Date.twentyMinutesAgo
+        return NSPredicate(format: "timestamp >= %@ AND bolus.isExternal == %@", date as NSDate, false as NSNumber)
+    }
+
     static func duplicateInLastFourLoops(_ date: Date) -> NSPredicate {
         let date20m = Date.twentyMinutesAgo
         return NSPredicate(format: "timestamp >= %@ && timestamp == %@", date20m as NSDate, date as NSDate)