Преглед изворни кода

refactoring in apsManager to use async await, fix wrong moc access in enactDetermination, fixes for smb offset in MainChart, fix glucose not updating in home view

polscm32 пре 1 година
родитељ
комит
a80425faf8

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -384,6 +384,7 @@
 		BDB3C1042C0341E600CEEAA1 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */; };
 		BDB3C1052C0341E600CEEAA1 /* TempBasalStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
+		BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */; };
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
@@ -1010,6 +1011,7 @@
 		BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
+		BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotification.swift; sourceTree = "<group>"; };
 		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
 		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
@@ -2236,6 +2238,7 @@
 				581AC4382BE22ED10038760C /* JSONConverter.swift */,
 				BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */,
 				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
+				BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -3176,6 +3179,7 @@
 				19012CDC291D2CB900FB8210 /* LoopStats.swift in Sources */,
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
+				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				5856174E2BDADA3F009B23D7 /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,

+ 110 - 123
FreeAPS/Sources/APS/APSManager.swift

@@ -12,7 +12,7 @@ import Swinject
 protocol APSManager {
     func heartbeat(date: Date)
     func autotune() -> AnyPublisher<Autotune?, Never>
-    func enactBolus(amount: Double, isSMB: Bool)
+    func enactBolus(amount: Double, isSMB: Bool) async
     var pumpManager: PumpManagerUI? { get set }
     var bluetoothManager: BluetoothStateManager? { get }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
@@ -295,8 +295,17 @@ final class BaseAPSManager: APSManager, Injectable {
 
                 self.nightscout.uploadStatus()
 
-                // Closed loop - enact suggested
-                return self.enactDetermination()
+                // Closed loop - enact Determination
+                return Future { promise in
+                    Task {
+                        do {
+                            try await self.enactDetermination()
+                            promise(.success(()))
+                        } catch {
+                            promise(.failure(error))
+                        }
+                    }
+                }.eraseToAnyPublisher()
             }
             .sink { [weak self] completion in
                 guard let self = self else { return }
@@ -471,7 +480,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private var bolusReporter: DoseProgressReporter?
 
-    func enactBolus(amount: Double, isSMB: Bool) {
+    func enactBolus(amount: Double, isSMB: Bool) async {
         if let error = verifyStatus() {
             processError(error)
             processQueue.async {
@@ -484,30 +493,29 @@ final class BaseAPSManager: APSManager, Injectable {
 
         guard let pump = pumpManager else { return }
 
-        let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
+        let roundedAmount = pump.roundToSupportedBolusVolume(units: amount)
 
-        debug(.apsManager, "Enact bolus \(roundedAmout), manual \(!isSMB)")
+        debug(.apsManager, "Enact bolus \(roundedAmount), manual \(!isSMB)")
 
-        pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
-            if case let .failure(error) = completion {
-                warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
-                self.processError(APSError.pumpError(error))
-                if !isSMB {
-                    self.processQueue.async {
-                        self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
-                            $0.bolusDidFail()
-                        }
+        do {
+            try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
+            debug(.apsManager, "Bolus succeeded")
+            if !isSMB {
+//                determineBasal()
+                determineBasalSync()
+            }
+            bolusProgress.send(0)
+        } catch {
+            warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
+            processError(APSError.pumpError(error))
+            if !isSMB {
+                processQueue.async {
+                    self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+                        $0.bolusDidFail()
                     }
                 }
-            } else {
-                debug(.apsManager, "Bolus succeeded")
-                if !isSMB {
-                    self.determineBasal().sink { _ in }.store(in: &self.lifetime)
-                }
-                self.bolusProgress.send(0)
             }
-        } receiveValue: { _ in }
-            .store(in: &lifetime)
+        }
     }
 
     func cancelBolus() {
@@ -699,7 +707,7 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func fetchDetermination() -> OrefDetermination? {
+    private func fetchDetermination() -> NSManagedObjectID? {
         CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             onContext: privateContext,
@@ -707,93 +715,77 @@ final class BaseAPSManager: APSManager, Injectable {
             key: "deliverAt",
             ascending: false,
             fetchLimit: 1
-        ).first
+        ).first?.objectID
     }
 
-    private func enactDetermination() -> AnyPublisher<Void, Error> {
-        // Fetch determination within the correct context
-        Future<OrefDetermination?, Error> { promise in
-            self.privateContext.perform {
-                let determination = self.fetchDetermination()
-                promise(.success(determination))
-            }
+    private func enactDetermination() async throws {
+        guard let determinationID = fetchDetermination() else {
+            throw APSError.apsError(message: "Determination not found")
         }
-        .flatMap { determination -> AnyPublisher<Void, Error> in
-            guard let determination = determination else {
-                return Fail(error: APSError.apsError(message: "Determination not found")).eraseToAnyPublisher()
-            }
 
-            guard let pump = self.pumpManager else {
-                return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
-            }
+        guard let pump = pumpManager else {
+            throw APSError.apsError(message: "Pump not set")
+        }
 
-            // Unable to do temp basal during manual temp basal 😁
-            if self.isManualTempBasal {
-                return Fail(error: APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp"))
-                    .eraseToAnyPublisher()
-            }
+        // Unable to do temp basal during manual temp basal 😁
+        if isManualTempBasal {
+            throw APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp")
+        }
 
-            let rateValue = determination.rate
-            let durationValue = determination.duration
-            let smbToDeliver = determination.smbToDeliver
+        let (rateDecimal, durationInSeconds, smbToDeliver) = try await setValues(determinationID: determinationID)
 
-            let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
-                if let error = self.verifyStatus() {
-                    return Fail(error: error).eraseToAnyPublisher()
-                }
+        try await performBasal(pump: pump, rate: rateDecimal, duration: durationInSeconds)
 
-                guard let rate = rateValue else {
-                    debug(.apsManager, "No temp required")
-                    return Just(()).setFailureType(to: Error.self)
-                        .eraseToAnyPublisher()
-                }
-                return pump.enactTempBasal(
-                    unitsPerHour: Double(truncating: rate as NSNumber),
-                    for: TimeInterval(durationValue * 60)
-                ).map { _ in
-                    let temp = TempBasal(
-                        duration: Int(durationValue),
-                        rate: ((rateValue ?? 0) as NSDecimalNumber) as Decimal,
-                        temp: .absolute,
-                        timestamp: Date()
-                    )
-                    self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
+        // only perform a bolus if smbToDeliver is > 0
+        if smbToDeliver.compare(NSDecimalNumber(value: 0)) == .orderedDescending {
+            try await performBolus(pump: pump, smbToDeliver: smbToDeliver)
+        }
+    }
 
-                    return ()
-                }
-                .eraseToAnyPublisher()
-            }.eraseToAnyPublisher()
+    private func setValues(determinationID: NSManagedObjectID) async throws -> (NSDecimalNumber, TimeInterval, NSDecimalNumber) {
+        return try await withCheckedThrowingContinuation { continuation in
+            self.privateContext.perform {
+                do {
+                    let determination = try self.privateContext.existingObject(with: determinationID) as? OrefDetermination
 
-            let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
-                if let error = self.verifyStatus() {
-                    return Fail(error: error).eraseToAnyPublisher()
-                }
-                guard let smbAmount = smbToDeliver else {
-                    debug(.apsManager, "No bolus required")
-                    return Just(()).setFailureType(to: Error.self)
-                        .eraseToAnyPublisher()
-                }
-                return pump.enactBolus(units: Double(truncating: smbAmount), automatic: true).map { _ in
-                    self.bolusProgress.send(0)
-                    return ()
-                }
-                .eraseToAnyPublisher()
-            }.eraseToAnyPublisher()
+                    /// Default values should be 0
+                    /// If we would use guard here Determine Basal would fail unnecessarily often
+                    let rate = (determination?.rate ?? 0) as NSDecimalNumber
+                    let duration = TimeInterval((determination?.duration ?? 0) * 60)
+                    let smbToDeliver = determination?.smbToDeliver ?? 0
 
-            return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
+                    continuation.resume(returning: (rate, duration, smbToDeliver))
+                } catch {
+                    continuation.resume(throwing: error)
+                }
+            }
         }
-        .eraseToAnyPublisher()
+    }
+
+    private func performBasal(pump: PumpManager, rate: NSDecimalNumber, duration: TimeInterval) async throws {
+        try await pump.enactTempBasal(unitsPerHour: Double(truncating: rate), for: duration)
+
+        let temp = TempBasal(
+            duration: Int(duration / 60),
+            rate: rate as Decimal,
+            temp: .absolute,
+            timestamp: Date()
+        )
+        storage.save(temp, as: OpenAPS.Monitor.tempBasal)
+    }
+
+    private func performBolus(pump: PumpManager, smbToDeliver: NSDecimalNumber) async throws {
+        try await pump.enactBolus(units: Double(truncating: smbToDeliver), automatic: true)
+        bolusProgress.send(0)
     }
 
     private func reportEnacted(received: Bool) {
         privateContext.performAndWait {
-            guard let determination = fetchDetermination(), determination.deliverAt != nil else {
+            guard let determinationID = fetchDetermination() else {
                 return
             }
 
-            let objectID = determination.objectID
-
-            if let determinationUpdated = self.privateContext.object(with: objectID) as? OrefDetermination {
+            if let determinationUpdated = self.privateContext.object(with: determinationID) as? OrefDetermination {
                 determinationUpdated.timestamp = Date()
                 determinationUpdated.received = received
 
@@ -806,28 +798,28 @@ final class BaseAPSManager: APSManager, Injectable {
                         "Failed  \(DebuggingIdentifiers.succeeded) to save context in reportEnacted(): \(error.localizedDescription)"
                     )
                 }
-            } else {
-                debugPrint("Failed to update OrefDetermination in reportEnacted()")
-            }
 
-            // TODO: - replace this...
-            let saveLastLoop = LastLoop(context: self.privateContext)
-            saveLastLoop.iob = (determination.iob ?? 0) as NSDecimalNumber
-            saveLastLoop.cob = determination.cob as? NSDecimalNumber
-            saveLastLoop.timestamp = (determination.timestamp ?? .distantPast) as Date
+                // TODO: - replace this...
+                let saveLastLoop = LastLoop(context: self.privateContext)
+                saveLastLoop.iob = (determinationUpdated.iob ?? 0) as NSDecimalNumber
+                saveLastLoop.cob = determinationUpdated.cob as? NSDecimalNumber
+                saveLastLoop.timestamp = (determinationUpdated.timestamp ?? .distantPast) as Date
 
-            do {
-                guard privateContext.hasChanges else { return }
-                try privateContext.save()
-            } catch {
-                print(error.localizedDescription)
-            }
+                do {
+                    guard privateContext.hasChanges else { return }
+                    try privateContext.save()
+                } catch {
+                    print(error.localizedDescription)
+                }
 
-            debug(.apsManager, "Determination enacted. Received: \(received)")
-        }
+                debug(.apsManager, "Determination enacted. Received: \(received)")
 
-        nightscout.uploadStatus()
-        statistics()
+                nightscout.uploadStatus()
+                statistics()
+            } else {
+                debugPrint("Failed to update OrefDetermination in reportEnacted()")
+            }
+        }
     }
 
     private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
@@ -1374,39 +1366,34 @@ final class BaseAPSManager: APSManager, Injectable {
 }
 
 private extension PumpManager {
-    func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) -> AnyPublisher<DoseEntry?, Error> {
-        Future { promise in
+    func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval) async throws {
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
             self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
                 if let error = error {
                     debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
-                    promise(.failure(error))
+                    continuation.resume(throwing: error)
                 } else {
                     debug(.apsManager, "Temp basal succeeded: \(unitsPerHour) for: \(duration)")
-                    promise(.success(nil))
+                    continuation.resume(returning: ())
                 }
             }
         }
-        .mapError { APSError.pumpError($0) }
-        .eraseToAnyPublisher()
     }
 
-    func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry?, Error> {
-        Future { promise in
-            // convert automatic
+    func enactBolus(units: Double, automatic: Bool) async throws {
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
             let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
 
             self.enactBolus(units: units, activationType: automaticValue) { error in
                 if let error = error {
                     debug(.apsManager, "Bolus failed: \(units)")
-                    promise(.failure(error))
+                    continuation.resume(throwing: error)
                 } else {
                     debug(.apsManager, "Bolus succeeded: \(units)")
-                    promise(.success(nil))
+                    continuation.resume(returning: ())
                 }
             }
         }
-        .mapError { APSError.pumpError($0) }
-        .eraseToAnyPublisher()
     }
 
     func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {

+ 5 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -89,6 +89,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 do {
                     try self.coredataContext.execute(batchInsert)
                     debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
+
+                    // Send notification for triggering a fetch in Home State Model to update the Glucose Array
+                    /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
+                    /// But I do not want to fetch on the Main Thread using the @FetchRequest property, I also can not use the FetchedResultsController because of the architecture of the State Model (it must inherit from BaseStateModel and therefore can not inherit from NSObject as well) and because of the fact that I am using a batch insert here there are no notifications sent from the managedObjectContext because changes are directly stored in the persistent container
+                    Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
                 } catch {
                     debugPrint(
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"

+ 1 - 5
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -287,9 +287,7 @@ extension Bolus {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
-                    apsManager.enactBolus(amount: maxAmount, isSMB: false)
-//                    savePumpInsulin(amount: amount)
-                    // already saved via pumphistory through apsManager
+                    await apsManager.enactBolus(amount: maxAmount, isSMB: false)
                 } else {
                     print("authentication failed")
                 }
@@ -570,7 +568,6 @@ 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(
@@ -614,7 +611,6 @@ extension Bolus.StateModel {
 // MARK: - Setup Glucose and Determinations
 
 extension Bolus.StateModel {
-    
     // Glucose
     private func setupGlucoseArray() {
         Task {

+ 17 - 3
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -474,6 +474,14 @@ extension Home.StateModel {
             name: Notification.Name.NSManagedObjectContextDidSave,
             object: nil
         )
+
+        /// custom notification that is sent when a batch insert of glucose objects is done
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleBatchInsert),
+            name: .didPerformBatchInsert,
+            object: nil
+        )
     }
 
     /// determine the actions when the context has changed
@@ -487,12 +495,17 @@ extension Home.StateModel {
         }
     }
 
+    @objc private func handleBatchInsert() {
+        setupGlucoseArray()
+    }
+
     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 manualGlucoseUpdates = 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 }
@@ -501,6 +514,8 @@ extension Home.StateModel {
         DispatchQueue.global(qos: .background).async {
             if !glucoseUpdates.isEmpty {
                 self.setupGlucoseArray()
+            }
+            if !manualGlucoseUpdates.isEmpty {
                 self.setupManualGlucoseArray()
             }
             if !determinationUpdates.isEmpty {
@@ -524,7 +539,6 @@ extension Home.StateModel {
 // MARK: - Handle Core Data changes and update Arrays to display them in the UI
 
 extension Home.StateModel {
-    
     // Setup Glucose
     private func setupGlucoseArray() {
         Task {
@@ -560,8 +574,8 @@ extension Home.StateModel {
     // Setup Manual Glucose
     private func setupManualGlucoseArray() {
         Task {
-            let ids = await self.fetchGlucose()
-            await updateGlucoseArray(with: ids)
+            let ids = await self.fetchManualGlucose()
+            await updateManualGlucoseArray(with: ids)
         }
     }
 

+ 12 - 10
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -330,16 +330,14 @@ extension MainChartView {
 
 extension MainChartView {
     private func drawBoluses() -> some ChartContent {
-        /// smbs in triangle form
         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
-            let yPosition = (Decimal(glucose) * conversionFactor) + bolusOffset
-            let size = (Config.bolusSize + CGFloat(truncating: amount) * Config.bolusScale) * 1.8
 
-            // don't display triangles if it is no smb
-            if amount != 0 {
+            if amount != 0, let glucose = timeToNearestGlucose(time: bolusDate.timeIntervalSince1970)?.glucose {
+                let yPosition = (Decimal(glucose) * conversionFactor) + bolusOffset
+                let size = (Config.bolusSize + CGFloat(truncating: amount) * Config.bolusScale) * 1.8
+
                 PointMark(
                     x: .value("Time", bolusDate, unit: .second),
                     y: .value("Value", yPosition)
@@ -646,17 +644,21 @@ extension MainChartView {
             return nil
         }
 
+        // sort by date
+        let sortedGlucose = state.glucoseFromPersistence
+            .sorted { $0.date?.timeIntervalSince1970 ?? 0 < $1.date?.timeIntervalSince1970 ?? 0 }
+
         var low = 0
-        var high = state.glucoseFromPersistence.count - 1
+        var high = sortedGlucose.count - 1
         var closestGlucose: GlucoseStored?
 
         // binary search to find next glucose
         while low <= high {
             let mid = low + (high - low) / 2
-            let midTime = state.glucoseFromPersistence[mid].date?.timeIntervalSince1970 ?? 0
+            let midTime = sortedGlucose[mid].date?.timeIntervalSince1970 ?? 0
 
             if midTime == time {
-                return state.glucoseFromPersistence[mid]
+                return sortedGlucose[mid]
             } else if midTime < time {
                 low = mid + 1
             } else {
@@ -665,7 +667,7 @@ extension MainChartView {
 
             // update if necessary
             if closestGlucose == nil || abs(midTime - time) < abs(closestGlucose!.date?.timeIntervalSince1970 ?? 0 - time) {
-                closestGlucose = state.glucoseFromPersistence[mid]
+                closestGlucose = sortedGlucose[mid]
             }
         }
 

+ 3 - 1
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -416,7 +416,9 @@ extension BaseWatchManager: WCSessionDelegate {
         }
 
         if let bolus = message["bolus"] as? Double, bolus > 0 {
-            apsManager.enactBolus(amount: bolus, isSMB: false)
+            Task {
+                await apsManager.enactBolus(amount: bolus, isSMB: false)
+            }
             replyHandler(["confirmation": true])
             return
         }

+ 7 - 0
Model/Helper/CustomNotification.swift

@@ -0,0 +1,7 @@
+import Foundation
+
+extension Notification.Name {
+    static let didPerformBatchInsert = Notification.Name("didPerformBatchInsert")
+    static let didPerformBatchUpdate = Notification.Name("didPerformBatchUpdate")
+    static let didPerformBatchDelete = Notification.Name("didPerformBatchDelete")
+}