Просмотр исходного кода

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 2 лет назад
Родитель
Сommit
a80425faf8

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

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

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

@@ -12,7 +12,7 @@ import Swinject
 protocol APSManager {
 protocol APSManager {
     func heartbeat(date: Date)
     func heartbeat(date: Date)
     func autotune() -> AnyPublisher<Autotune?, Never>
     func autotune() -> AnyPublisher<Autotune?, Never>
-    func enactBolus(amount: Double, isSMB: Bool)
+    func enactBolus(amount: Double, isSMB: Bool) async
     var pumpManager: PumpManagerUI? { get set }
     var pumpManager: PumpManagerUI? { get set }
     var bluetoothManager: BluetoothStateManager? { get }
     var bluetoothManager: BluetoothStateManager? { get }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
@@ -295,8 +295,17 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
                 self.nightscout.uploadStatus()
                 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
             .sink { [weak self] completion in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -471,7 +480,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     private var bolusReporter: DoseProgressReporter?
     private var bolusReporter: DoseProgressReporter?
 
 
-    func enactBolus(amount: Double, isSMB: Bool) {
+    func enactBolus(amount: Double, isSMB: Bool) async {
         if let error = verifyStatus() {
         if let error = verifyStatus() {
             processError(error)
             processError(error)
             processQueue.async {
             processQueue.async {
@@ -484,30 +493,29 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
         guard let pump = pumpManager else { return }
         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() {
     func cancelBolus() {
@@ -699,7 +707,7 @@ final class BaseAPSManager: APSManager, Injectable {
         }
         }
     }
     }
 
 
-    private func fetchDetermination() -> OrefDetermination? {
+    private func fetchDetermination() -> NSManagedObjectID? {
         CoreDataStack.shared.fetchEntities(
         CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: privateContext,
             onContext: privateContext,
@@ -707,93 +715,77 @@ final class BaseAPSManager: APSManager, Injectable {
             key: "deliverAt",
             key: "deliverAt",
             ascending: false,
             ascending: false,
             fetchLimit: 1
             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) {
     private func reportEnacted(received: Bool) {
         privateContext.performAndWait {
         privateContext.performAndWait {
-            guard let determination = fetchDetermination(), determination.deliverAt != nil else {
+            guard let determinationID = fetchDetermination() else {
                 return
                 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.timestamp = Date()
                 determinationUpdated.received = received
                 determinationUpdated.received = received
 
 
@@ -806,28 +798,28 @@ final class BaseAPSManager: APSManager, Injectable {
                         "Failed  \(DebuggingIdentifiers.succeeded) to save context in reportEnacted(): \(error.localizedDescription)"
                         "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 {
     private func roundDecimal(_ decimal: Decimal, _ digits: Double) -> Decimal {
@@ -1374,39 +1366,34 @@ final class BaseAPSManager: APSManager, Injectable {
 }
 }
 
 
 private extension PumpManager {
 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
             self.enactTempBasal(unitsPerHour: unitsPerHour, for: duration) { error in
                 if let error = error {
                 if let error = error {
                     debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
                     debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
-                    promise(.failure(error))
+                    continuation.resume(throwing: error)
                 } else {
                 } else {
                     debug(.apsManager, "Temp basal succeeded: \(unitsPerHour) for: \(duration)")
                     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
             let automaticValue = automatic ? BolusActivationType.automatic : BolusActivationType.manualRecommendationAccepted
 
 
             self.enactBolus(units: units, activationType: automaticValue) { error in
             self.enactBolus(units: units, activationType: automaticValue) { error in
                 if let error = error {
                 if let error = error {
                     debug(.apsManager, "Bolus failed: \(units)")
                     debug(.apsManager, "Bolus failed: \(units)")
-                    promise(.failure(error))
+                    continuation.resume(throwing: error)
                 } else {
                 } else {
                     debug(.apsManager, "Bolus succeeded: \(units)")
                     debug(.apsManager, "Bolus succeeded: \(units)")
-                    promise(.success(nil))
+                    continuation.resume(returning: ())
                 }
                 }
             }
             }
         }
         }
-        .mapError { APSError.pumpError($0) }
-        .eraseToAnyPublisher()
     }
     }
 
 
     func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
     func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {

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

@@ -89,6 +89,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 do {
                 do {
                     try self.coredataContext.execute(batchInsert)
                     try self.coredataContext.execute(batchInsert)
                     debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
                     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 {
                 } catch {
                     debugPrint(
                     debugPrint(
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
                         "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 {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
                 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 {
                 } else {
                     print("authentication failed")
                     print("authentication failed")
                 }
                 }
@@ -570,7 +568,6 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 // MARK: - Setup Notifications
 // MARK: - Setup Notifications
 
 
 extension Bolus.StateModel {
 extension Bolus.StateModel {
-
     /// listens for the notifications sent when the managedObjectContext has saved!
     /// listens for the notifications sent when the managedObjectContext has saved!
     func setupNotification() {
     func setupNotification() {
         Foundation.NotificationCenter.default.addObserver(
         Foundation.NotificationCenter.default.addObserver(
@@ -614,7 +611,6 @@ extension Bolus.StateModel {
 // MARK: - Setup Glucose and Determinations
 // MARK: - Setup Glucose and Determinations
 
 
 extension Bolus.StateModel {
 extension Bolus.StateModel {
-    
     // Glucose
     // Glucose
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task {
         Task {

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

@@ -474,6 +474,14 @@ extension Home.StateModel {
             name: Notification.Name.NSManagedObjectContextDidSave,
             name: Notification.Name.NSManagedObjectContextDidSave,
             object: nil
             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
     /// 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 {
     private func processUpdates(userInfo: [AnyHashable: Any]) async {
         var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
         var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
         objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
         objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
         objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
         objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
 
 
         let glucoseUpdates = objects.filter { $0 is GlucoseStored }
         let glucoseUpdates = objects.filter { $0 is GlucoseStored }
+        let manualGlucoseUpdates = objects.filter { $0 is GlucoseStored }
         let determinationUpdates = objects.filter { $0 is OrefDetermination }
         let determinationUpdates = objects.filter { $0 is OrefDetermination }
         let carbUpdates = objects.filter { $0 is CarbEntryStored }
         let carbUpdates = objects.filter { $0 is CarbEntryStored }
         let insulinUpdates = objects.filter { $0 is PumpEventStored }
         let insulinUpdates = objects.filter { $0 is PumpEventStored }
@@ -501,6 +514,8 @@ extension Home.StateModel {
         DispatchQueue.global(qos: .background).async {
         DispatchQueue.global(qos: .background).async {
             if !glucoseUpdates.isEmpty {
             if !glucoseUpdates.isEmpty {
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
+            }
+            if !manualGlucoseUpdates.isEmpty {
                 self.setupManualGlucoseArray()
                 self.setupManualGlucoseArray()
             }
             }
             if !determinationUpdates.isEmpty {
             if !determinationUpdates.isEmpty {
@@ -524,7 +539,6 @@ extension Home.StateModel {
 // MARK: - Handle Core Data changes and update Arrays to display them in the UI
 // MARK: - Handle Core Data changes and update Arrays to display them in the UI
 
 
 extension Home.StateModel {
 extension Home.StateModel {
-    
     // Setup Glucose
     // Setup Glucose
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task {
         Task {
@@ -560,8 +574,8 @@ extension Home.StateModel {
     // Setup Manual Glucose
     // Setup Manual Glucose
     private func setupManualGlucoseArray() {
     private func setupManualGlucoseArray() {
         Task {
         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 {
 extension MainChartView {
     private func drawBoluses() -> some ChartContent {
     private func drawBoluses() -> some ChartContent {
-        /// smbs in triangle form
         ForEach(state.insulinFromPersistence) { insulin in
         ForEach(state.insulinFromPersistence) { insulin in
             let amount = insulin.bolus?.amount ?? 0 as NSDecimalNumber
             let amount = insulin.bolus?.amount ?? 0 as NSDecimalNumber
             let bolusDate = insulin.timestamp ?? Date()
             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(
                 PointMark(
                     x: .value("Time", bolusDate, unit: .second),
                     x: .value("Time", bolusDate, unit: .second),
                     y: .value("Value", yPosition)
                     y: .value("Value", yPosition)
@@ -646,17 +644,21 @@ extension MainChartView {
             return nil
             return nil
         }
         }
 
 
+        // sort by date
+        let sortedGlucose = state.glucoseFromPersistence
+            .sorted { $0.date?.timeIntervalSince1970 ?? 0 < $1.date?.timeIntervalSince1970 ?? 0 }
+
         var low = 0
         var low = 0
-        var high = state.glucoseFromPersistence.count - 1
+        var high = sortedGlucose.count - 1
         var closestGlucose: GlucoseStored?
         var closestGlucose: GlucoseStored?
 
 
         // binary search to find next glucose
         // binary search to find next glucose
         while low <= high {
         while low <= high {
             let mid = low + (high - low) / 2
             let mid = low + (high - low) / 2
-            let midTime = state.glucoseFromPersistence[mid].date?.timeIntervalSince1970 ?? 0
+            let midTime = sortedGlucose[mid].date?.timeIntervalSince1970 ?? 0
 
 
             if midTime == time {
             if midTime == time {
-                return state.glucoseFromPersistence[mid]
+                return sortedGlucose[mid]
             } else if midTime < time {
             } else if midTime < time {
                 low = mid + 1
                 low = mid + 1
             } else {
             } else {
@@ -665,7 +667,7 @@ extension MainChartView {
 
 
             // update if necessary
             // update if necessary
             if closestGlucose == nil || abs(midTime - time) < abs(closestGlucose!.date?.timeIntervalSince1970 ?? 0 - time) {
             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 {
         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])
             replyHandler(["confirmation": true])
             return
             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")
+}