Procházet zdrojové kódy

Async rework 2.0 (#50)

* make apsManager and openAPS async....highly wip

* cleanup

* clean up

* test

* cleanup

* small fixes for LA, refactoring

* refactoring of async functions

* cleanup

* async save function for Determination

* address PR feedback (better error handling)

---------

Co-authored-by: Marvin Polscheit <marvinpolscheit@mac-mini.speedport.ip>
polscm32 před 1 rokem
rodič
revize
f18f79d798

+ 2 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -705,7 +705,6 @@
 		3811DEE725CA063400A708ED /* PersistedProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistedProperty.swift; sourceTree = "<group>"; };
 		3811DF0125CA9FEA00A708ED /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
 		3811DF0F25CAAAE200A708ED /* APSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APSManager.swift; sourceTree = "<group>"; };
-		3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		3818AA45274C229000843DB3 /* LibreTransmitter */ = {isa = PBXFileReference; lastKnownFileType = folder; name = LibreTransmitter; path = Dependencies/LibreTransmitter; sourceTree = "<group>"; };
 		3818AA49274C267000843DB3 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3818AA4C274C26A300843DB3 /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -953,6 +952,7 @@
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
+		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1725,8 +1725,8 @@
 			isa = PBXGroup;
 			children = (
 				587A54C82BCDCE0F009D38E2 /* Model */,
+				BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
-				3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */,
 				388E595A25AD948C0019842D /* FreeAPS */,
 				38FCF3EE25E9028E0078B0D1 /* FreeAPSTests */,
 				3818AA44274C229000843DB3 /* Packages */,

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 584 - 490
FreeAPS/Sources/APS/APSManager.swift


+ 0 - 47
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -43,10 +43,6 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
     MockPumpManager.managerIdentifier: MockPumpManager.self
 ]
 
-// private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = staticPumpManagers.reduce(into: [:]) { map, Type in
-//    map[Type.managerIdentifier] = Type
-// }
-
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
@@ -164,20 +160,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 self.updateUpdateFinished(true)
             }
         }
-
-//        pumpUpdateCancellable = Future<Bool, Never> { [unowned self] promise in
-//            pumpUpdatePromise = promise
-//            debug(.deviceManager, "Waiting for pump update and loop recommendation")
-//            processQueue.safeSync {
-//                pumpManager.ensureCurrentPumpData { _ in
-//                    debug(.deviceManager, "Pump data updated.")
-//                }
-//            }
-//        }
-//        .timeout(30, scheduler: processQueue)
-//        .replaceError(with: false)
-//        .replaceEmpty(with: false)
-//        .sink(receiveValue: updateUpdateFinished)
     }
 
     private func updateUpdateFinished(_ recommendsLoop: Bool) {
@@ -186,12 +168,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         if !recommendsLoop {
             warning(.deviceManager, "Loop recommendation time out or got error. Trying to loop right now.")
         }
-
-        // directly in loop() function
-//        guard !loopInProgress else {
-//            warning(.deviceManager, "Loop already in progress. Skip recommendation.")
-//            return
-//        }
         self.recommendsLoop.send()
     }
 
@@ -340,29 +316,6 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
         }
 
         let batteryPercent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
-        let battery = Battery(
-            percent: batteryPercent,
-            voltage: nil,
-            string: batteryPercent >= 10 ? .normal : .low,
-            display: pumpManager.status.pumpBatteryChargeRemaining != nil
-        )
-
-        privateContext.perform {
-            let batteryToStore = OpenAPS_Battery(context: self.privateContext)
-            batteryToStore.id = UUID()
-            batteryToStore.date = Date()
-            batteryToStore.percent = Int16(batteryPercent)
-            batteryToStore.voltage = nil
-            batteryToStore.status = batteryPercent > 10 ? "normal" : "low"
-            batteryToStore.display = status.pumpBatteryChargeRemaining != nil
-
-            do {
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
-            } catch {
-                print(error.localizedDescription)
-            }
-        }
         broadcaster.notify(PumpTimeZoneObserver.self, on: processQueue) {
             $0.pumpTimeZoneDidChange(status.timeZone)
         }

+ 185 - 171
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -30,8 +30,8 @@ final class OpenAPS {
     }
 
     // Use the helper function for cleaner code
-    func processDetermination(_ determination: Determination) {
-        context.perform {
+    func processDetermination(_ determination: Determination) async {
+        await context.perform {
             let newOrefDetermination = OrefDetermination(context: self.context)
             newOrefDetermination.id = UUID()
 
@@ -48,7 +48,7 @@ final class OpenAPS {
             newOrefDetermination.temp = determination.temp?.rawValue ?? "absolute"
             newOrefDetermination.rate = self.decimalToNSDecimalNumber(determination.rate)
             newOrefDetermination.reason = determination.reason
-            newOrefDetermination.duration = Int16(determination.duration ?? 0)
+            newOrefDetermination.duration = self.decimalToNSDecimalNumber(determination.duration)
             newOrefDetermination.iob = self.decimalToNSDecimalNumber(determination.iob)
             newOrefDetermination.threshold = self.decimalToNSDecimalNumber(determination.threshold)
             newOrefDetermination.minDelta = self.decimalToNSDecimalNumber(determination.minDelta)
@@ -82,86 +82,77 @@ final class OpenAPS {
                         }
                     }
             }
-
-            self.attemptToSaveContext()
         }
+        await attemptToSaveContext()
     }
 
-    func attemptToSaveContext() {
-        do {
-            guard context.hasChanges else { return }
-            try context.save()
-        } catch {
-            print(error.localizedDescription)
+    func attemptToSaveContext() async {
+        await context.perform {
+            do {
+                guard self.context.hasChanges else { return }
+                try self.context.save()
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Determination to Core Data")
+            }
         }
     }
 
     // fetch glucose to pass it to the meal function and to determine basal
-    private func fetchAndProcessGlucose() -> String {
-        var glucoseAsJSON: String?
-
-        context.performAndWait {
-            let results = CoreDataStack.shared.fetchEntities(
-                ofType: GlucoseStored.self,
-                onContext: context,
-                predicate: NSPredicate.predicateForSixHoursAgo,
-                key: "date",
-                ascending: false,
-                fetchLimit: 72,
-                batchSize: 24
-            )
-
+    private func fetchAndProcessGlucose() async -> String {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateForSixHoursAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 72,
+            batchSize: 24
+        )
+
+        return await context.perform {
             // convert to json
-            glucoseAsJSON = self.jsonConverter.convertToJSON(results)
+            return self.jsonConverter.convertToJSON(results)
         }
-
-        return glucoseAsJSON ?? "{}"
     }
 
-    private func fetchAndProcessCarbs() -> String {
-        // perform fetch AND conversion on the same thread
-        // if we do it like this we do not change the thread and do not have to pass the objectIDs
-        var carbsAsJSON: String?
-
-        context.performAndWait {
-            let results = CoreDataStack.shared.fetchEntities(
-                ofType: CarbEntryStored.self,
-                onContext: context,
-                predicate: NSPredicate.predicateForOneDayAgo,
-                key: "date",
-                ascending: false
-            )
-
-            // convert to json
-            carbsAsJSON = self.jsonConverter.convertToJSON(results)
+    private func fetchAndProcessCarbs() async -> String {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateForOneDayAgo,
+            key: "date",
+            ascending: false
+        )
+
+        // convert to json
+        return await context.perform {
+            return self.jsonConverter.convertToJSON(results)
         }
-
-        return carbsAsJSON ?? "{}"
     }
 
-    private func fetchPumpHistoryObjectIDs() -> [NSManagedObjectID]? {
-        context.performAndWait {
-            let results = CoreDataStack.shared.fetchEntities(
-                ofType: PumpEventStored.self,
-                onContext: context,
-                predicate: NSPredicate.pumpHistoryLast24h,
-                key: "timestamp",
-                ascending: false,
-                batchSize: 50
-            )
+    private func fetchPumpHistoryObjectIDs() async -> [NSManagedObjectID]? {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpHistoryLast24h,
+            key: "timestamp",
+            ascending: false,
+            batchSize: 50
+        )
+        return await context.perform {
             return results.map(\.objectID)
         }
     }
 
-    private func parsePumpHistory(_ pumpHistoryObjectIDs: [NSManagedObjectID]) -> String {
+    private func parsePumpHistory(_ pumpHistoryObjectIDs: [NSManagedObjectID]) async -> String {
         // Return an empty JSON object if the list of object IDs is empty
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
 
         // Execute all operations on the background context
-        let jsonResult = context.performAndWait {
+        return await context.perform {
             // Load the pump events from the object IDs
             let pumpHistory: [PumpEventStored] = pumpHistoryObjectIDs
-                .compactMap { context.object(with: $0) as? PumpEventStored }
+                .compactMap { self.context.object(with: $0) as? PumpEventStored }
 
             // Create the DTOs
             let dtos: [PumpEventDTO] = pumpHistory.flatMap { event -> [PumpEventDTO] in
@@ -179,42 +170,45 @@ final class OpenAPS {
             }
 
             // Convert the DTOs to JSON
-            return jsonConverter.convertToJSON(dtos)
+            return self.jsonConverter.convertToJSON(dtos)
         }
-
-        // Return the JSON result
-        return jsonResult
     }
 
-    func determineBasal(currentTemp: TempBasal, clock: Date = Date()) -> Future<Determination?, Never> {
-        Future { promise in
-            self.processQueue.async {
-                debug(.openAPS, "Start determineBasal")
-
-                // clock
-                let dateFormatted = OpenAPS.dateFormatter.string(from: clock)
-                let dateFormattedAsString = "\"\(dateFormatted)\""
+    func determineBasal(currentTemp: TempBasal, clock: Date = Date()) async throws -> Determination? {
+        debug(.openAPS, "Start determineBasal")
 
-                // temp_basal
-                let tempBasal = currentTemp.rawJSON
+        // clock
+        let dateFormatted = OpenAPS.dateFormatter.string(from: clock)
+        let dateFormattedAsString = "\"\(dateFormatted)\""
 
-                let pumpHistoryObjectIDs = self.fetchPumpHistoryObjectIDs() ?? []
-                let pumpHistoryJSON = self.parsePumpHistory(pumpHistoryObjectIDs)
+        // temp_basal
+        let tempBasal = currentTemp.rawJSON
 
-                // carbs
-                let carbsAsJSON = self.fetchAndProcessCarbs()
+        // Perform asynchronous calls in parallel
+        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
+        async let carbs = fetchAndProcessCarbs()
+        async let glucose = fetchAndProcessGlucose()
+        async let oref2 = oref2()
 
-                // glucose
-                let glucoseAsJSON = self.fetchAndProcessGlucose()
+        // Await the results of asynchronous tasks
+        let pumpHistoryJSON = await parsePumpHistory(await pumpHistoryObjectIDs)
+        let carbsAsJSON = await carbs
+        let glucoseAsJSON = await glucose
+        let oref2_variables = await oref2
 
-                // TODO: - Save and fetch profile/basalProfile in/from UserDefaults
+        // TODO: - Save and fetch profile/basalProfile in/from UserDefaults!
 
-                // profile
-                let profile = self.loadFileFromStorage(name: Settings.profile)
-                let basalProfile = self.loadFileFromStorage(name: Settings.basalProfile)
+        // Load files from Storage
+        let profile = loadFileFromStorage(name: Settings.profile)
+        let basalProfile = loadFileFromStorage(name: Settings.basalProfile)
+        let autosens = loadFileFromStorage(name: Settings.autosense)
+        let reservoir = loadFileFromStorage(name: Monitor.reservoir)
+        let preferences = loadFileFromStorage(name: Settings.preferences)
 
-                // meal
-                let meal = self.meal(
+        // Meal
+        let meal: RawJSON = await withCheckedContinuation { continuation in
+            self.processQueue.async {
+                let result = self.meal(
                     pumphistory: pumpHistoryJSON,
                     profile: profile,
                     basalProfile: basalProfile,
@@ -222,24 +216,29 @@ final class OpenAPS {
                     carbs: carbsAsJSON,
                     glucose: glucoseAsJSON
                 )
+                continuation.resume(returning: result)
+            }
+        }
 
-                // iob
-                let autosens = self.loadFileFromStorage(name: Settings.autosense)
-                let iob = self.iob(
+        // IOB
+        let iob: RawJSON = await withCheckedContinuation { continuation in
+            self.processQueue.async {
+                let result = self.iob(
                     pumphistory: pumpHistoryJSON,
                     profile: profile,
                     clock: dateFormattedAsString,
                     autosens: autosens.isEmpty ? .null : autosens
                 )
+                continuation.resume(returning: result)
+            }
+        }
 
-                self.storage.save(iob, as: Monitor.iob)
-
-                // determine-basal
-                let reservoir = self.loadFileFromStorage(name: Monitor.reservoir)
-                let preferences = self.loadFileFromStorage(name: Settings.preferences)
-                let oref2_variables = self.oref2()
+        storage.save(iob, as: Monitor.iob)
 
-                let orefDetermination = self.determineBasal(
+        // Determine basal
+        let orefDetermination: RawJSON = await withCheckedContinuation { continuation in
+            self.processQueue.async {
+                let result = self.determineBasal(
                     glucose: glucoseAsJSON,
                     currentTemp: tempBasal,
                     iob: iob,
@@ -253,25 +252,27 @@ final class OpenAPS {
                     basalProfile: basalProfile,
                     oref2_variables: oref2_variables
                 )
-                debug(.openAPS, "Determinated: \(orefDetermination)")
+                continuation.resume(returning: result)
+            }
+        }
 
-                if var determination = Determination(from: orefDetermination) {
-                    determination.timestamp = determination.deliverAt ?? clock
+        debug(.openAPS, "Determinated: \(orefDetermination)")
 
-                    // save to core data asynchronously
-                    self.processDetermination(determination)
+        if var determination = Determination(from: orefDetermination) {
+            determination.timestamp = determination.deliverAt ?? clock
 
-                    promise(.success(determination))
-                } else {
-                    promise(.success(nil))
-                }
-            }
+            // save to core data asynchronously
+            await processDetermination(determination)
+
+            return determination
+        } else {
+            return nil
         }
     }
 
-    func oref2() -> RawJSON {
-        context.performAndWait {
-            let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
+    func oref2() async -> RawJSON {
+        await context.perform {
+            let preferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             var hbt_ = preferences?.halfBasalExerciseTarget ?? 160
             let wp = preferences?.weightPercentage ?? 1
             let smbMinutes = (preferences?.maxSMBBasalMinutes ?? 30) as NSDecimalNumber
@@ -286,28 +287,28 @@ final class OpenAPS {
             requestTDD.propertiesToFetch = ["timestamp", "totalDailyDose"]
             let sortTDD = NSSortDescriptor(key: "timestamp", ascending: true)
             requestTDD.sortDescriptors = [sortTDD]
-            try? uniqueEvents = context.fetch(requestTDD)
+            try? uniqueEvents = self.context.fetch(requestTDD)
 
             var sliderArray = [TempTargetsSlider]()
             let requestIsEnbled = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
             let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
             requestIsEnbled.sortDescriptors = [sortIsEnabled]
             // requestIsEnbled.fetchLimit = 1
-            try? sliderArray = context.fetch(requestIsEnbled)
+            try? sliderArray = self.context.fetch(requestIsEnbled)
 
             var overrideArray = [Override]()
             let requestOverrides = Override.fetchRequest() as NSFetchRequest<Override>
             let sortOverride = NSSortDescriptor(key: "date", ascending: false)
             requestOverrides.sortDescriptors = [sortOverride]
             // requestOverrides.fetchLimit = 1
-            try? overrideArray = context.fetch(requestOverrides)
+            try? overrideArray = self.context.fetch(requestOverrides)
 
             var tempTargetsArray = [TempTargets]()
             let requestTempTargets = TempTargets.fetchRequest() as NSFetchRequest<TempTargets>
             let sortTT = NSSortDescriptor(key: "date", ascending: false)
             requestTempTargets.sortDescriptors = [sortTT]
             requestTempTargets.fetchLimit = 1
-            try? tempTargetsArray = context.fetch(requestTempTargets)
+            try? tempTargetsArray = self.context.fetch(requestTempTargets)
 
             let total = uniqueEvents.compactMap({ each in each.totalDailyDose as? Decimal ?? 0 }).reduce(0, +)
             var indeces = uniqueEvents.count
@@ -422,7 +423,7 @@ final class OpenAPS {
                     smbMinutes: (overrideArray.first?.smbMinutes ?? smbMinutes) as Decimal,
                     uamMinutes: (overrideArray.first?.uamMinutes ?? uamMinutes) as Decimal
                 )
-                storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
+                self.storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
                 return self.loadFileFromStorage(name: Monitor.oref2_variables)
 
             } else {
@@ -450,31 +451,34 @@ final class OpenAPS {
                     smbMinutes: (overrideArray.first?.smbMinutes ?? smbMinutes) as Decimal,
                     uamMinutes: (overrideArray.first?.uamMinutes ?? uamMinutes) as Decimal
                 )
-                storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
+                self.storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
                 return self.loadFileFromStorage(name: Monitor.oref2_variables)
             }
         }
     }
 
-    func autosense() -> Future<Autosens?, Never> {
-        Future { promise in
-            self.processQueue.async {
-                debug(.openAPS, "Start autosens")
+    func autosense() async throws -> Autosens? {
+        debug(.openAPS, "Start autosens")
 
-                // pump history
-                let pumpHistoryObjectIDs = self.fetchPumpHistoryObjectIDs() ?? []
-                let pumpHistoryJSON = self.parsePumpHistory(pumpHistoryObjectIDs)
+        // Perform asynchronous calls in parallel
+        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
+        async let carbs = fetchAndProcessCarbs()
+        async let glucose = fetchAndProcessGlucose()
 
-                // carbs
-                let carbsAsJSON = self.fetchAndProcessCarbs()
+        // Await the results of asynchronous tasks
+        let pumpHistoryJSON = await parsePumpHistory(await pumpHistoryObjectIDs)
+        let carbsAsJSON = await carbs
+        let glucoseAsJSON = await glucose
 
-                /// glucose
-                let glucoseAsJSON = self.fetchAndProcessGlucose()
+        // Load files from Storage
+        let profile = loadFileFromStorage(name: Settings.profile)
+        let basalProfile = loadFileFromStorage(name: Settings.basalProfile)
+        let tempTargets = loadFileFromStorage(name: Settings.tempTargets)
 
-                let profile = self.loadFileFromStorage(name: Settings.profile)
-                let basalProfile = self.loadFileFromStorage(name: Settings.basalProfile)
-                let tempTargets = self.loadFileFromStorage(name: Settings.tempTargets)
-                let autosensResult = self.autosense(
+        // Autosense
+        let autosenseResult: RawJSON = await withCheckedContinuation { continuation in
+            self.processQueue.async {
+                let result = self.autosense(
                     glucose: glucoseAsJSON,
                     pumpHistory: pumpHistoryJSON,
                     basalprofile: basalProfile,
@@ -482,38 +486,43 @@ final class OpenAPS {
                     carbs: carbsAsJSON,
                     temptargets: tempTargets
                 )
-
-                debug(.openAPS, "AUTOSENS: \(autosensResult)")
-                if var autosens = Autosens(from: autosensResult) {
-                    autosens.timestamp = Date()
-                    self.storage.save(autosens, as: Settings.autosense)
-                    promise(.success(autosens))
-                } else {
-                    promise(.success(nil))
-                }
+                continuation.resume(returning: result)
             }
         }
-    }
 
-    func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) -> Future<Autotune?, Never> {
-        Future { promise in
-            self.processQueue.async {
-                debug(.openAPS, "Start autotune")
+        debug(.openAPS, "AUTOSENS: \(autosenseResult)")
+        if var autosens = Autosens(from: autosenseResult) {
+            autosens.timestamp = Date()
+            storage.save(autosens, as: Settings.autosense)
+
+            return autosens
+        } else {
+            return nil
+        }
+    }
 
-                // pump history
-                let pumpHistoryObjectIDs = self.fetchPumpHistoryObjectIDs() ?? []
-                let pumpHistoryJSON = self.parsePumpHistory(pumpHistoryObjectIDs)
+    func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) async -> Autotune? {
+        debug(.openAPS, "Start autotune")
 
-                /// glucose
-                let glucoseAsJSON = self.fetchAndProcessGlucose()
+        // Perform asynchronous calls in parallel
+        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
+        async let carbs = fetchAndProcessCarbs()
+        async let glucose = fetchAndProcessGlucose()
 
-                let profile = self.loadFileFromStorage(name: Settings.profile)
-                let pumpProfile = self.loadFileFromStorage(name: Settings.pumpProfile)
+        // Await the results of asynchronous tasks
+        let pumpHistoryJSON = await parsePumpHistory(await pumpHistoryObjectIDs)
+        let carbsAsJSON = await carbs
+        let glucoseAsJSON = await glucose
 
-                // carbs
-                let carbsAsJSON = self.fetchAndProcessCarbs()
+        // Load files from storage
+        let profile = loadFileFromStorage(name: Settings.profile)
+        let pumpProfile = loadFileFromStorage(name: Settings.pumpProfile)
+        let previousAutotune = storage.retrieve(Settings.autotune, as: RawJSON.self)
 
-                let autotunePreppedGlucose = self.autotunePrepare(
+        // Autotune
+        let autotunePreppedGlucose: RawJSON = await withCheckedContinuation { continuation in
+            self.processQueue.async {
+                let result = self.autotunePrepare(
                     pumphistory: pumpHistoryJSON,
                     profile: profile,
                     glucose: glucoseAsJSON,
@@ -522,32 +531,38 @@ final class OpenAPS {
                     categorizeUamAsBasal: categorizeUamAsBasal,
                     tuneInsulinCurve: tuneInsulinCurve
                 )
-                debug(.openAPS, "AUTOTUNE PREP: \(autotunePreppedGlucose)")
+                continuation.resume(returning: result)
+            }
+        }
 
-                let previousAutotune = self.storage.retrieve(Settings.autotune, as: RawJSON.self)
+        debug(.openAPS, "AUTOTUNE PREP: \(autotunePreppedGlucose)")
 
-                let autotuneResult = self.autotuneRun(
+        let autotuneResult: RawJSON = await withCheckedContinuation { continuation in
+            self.processQueue.async {
+                let result = self.autotuneRun(
                     autotunePreparedData: autotunePreppedGlucose,
                     previousAutotuneResult: previousAutotune ?? profile,
                     pumpProfile: pumpProfile
                 )
+                continuation.resume(returning: result)
+            }
+        }
 
-                debug(.openAPS, "AUTOTUNE RESULT: \(autotuneResult)")
+        debug(.openAPS, "AUTOTUNE RESULT: \(autotuneResult)")
 
-                if let autotune = Autotune(from: autotuneResult) {
-                    self.storage.save(autotuneResult, as: Settings.autotune)
-                    promise(.success(autotune))
-                } else {
-                    promise(.success(nil))
-                }
-            }
+        if let autotune = Autotune(from: autotuneResult) {
+            storage.save(autotuneResult, as: Settings.autotune)
+
+            return autotune
+        } else {
+            return nil
         }
     }
 
-    func makeProfiles(useAutotune: Bool) -> Future<Autotune?, Never> {
-        Future { promise in
+    func makeProfiles(useAutotune: Bool) async -> Autotune? {
+        await withCheckedContinuation { continuation in
             debug(.openAPS, "Start makeProfiles")
-            self.processQueue.async {
+            processQueue.async {
                 var preferences = self.loadFileFromStorage(name: Settings.preferences)
                 if preferences.isEmpty {
                     preferences = Preferences().rawJSON
@@ -592,11 +607,10 @@ final class OpenAPS {
                 self.storage.save(profile, as: Settings.profile)
 
                 if let tunedProfile = Autotune(from: profile) {
-                    promise(.success(tunedProfile))
-                    return
+                    continuation.resume(returning: tunedProfile)
+                } else {
+                    continuation.resume(returning: nil)
                 }
-
-                promise(.success(nil))
             }
         }
     }

+ 1 - 1
FreeAPS/Sources/Models/Determination.swift

@@ -7,7 +7,7 @@ struct Determination: JSON, Equatable {
     let eventualBG: Int?
     let sensitivityRatio: Decimal?
     let rate: Decimal?
-    let duration: Int?
+    let duration: Decimal?
     let iob: Decimal?
     let cob: Decimal?
     var predictions: Predictions?

+ 0 - 1
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigDataFlow.swift

@@ -6,6 +6,5 @@ enum AutotuneConfig {
 
 protocol AutotuneConfigProvider: Provider {
     var autotune: Autotune? { get }
-    func runAutotune() -> AnyPublisher<Autotune?, Never>
     func deleteAutotune()
 }

+ 0 - 4
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigProvider.swift

@@ -8,10 +8,6 @@ extension AutotuneConfig {
             storage.retrieve(OpenAPS.Settings.autotune, as: Autotune.self)
         }
 
-        func runAutotune() -> AnyPublisher<Autotune?, Never> {
-            apsManager.autotune()
-        }
-
         func deleteAutotune() {
             storage.remove(OpenAPS.Settings.autotune)
         }

+ 27 - 15
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigStateModel.swift

@@ -33,33 +33,45 @@ extension AutotuneConfig {
                         return Just(false).eraseToAnyPublisher()
                     }
                     self.settingsManager.settings.useAutotune = use
-                    return self.apsManager.makeProfiles()
+                    return Future { promise in
+                        Task.init(priority: .background) {
+                            do {
+                                _ = try await self.apsManager.makeProfiles()
+                                promise(.success(true))
+
+                            } catch {
+                                promise(.success(false))
+                            }
+                        }
+                    }
+                    .eraseToAnyPublisher()
                 }
                 .cancellable()
                 .store(in: &lifetime)
         }
 
         func run() {
-            provider.runAutotune()
-                .receive(on: DispatchQueue.main)
-                .flatMap { [weak self] result -> AnyPublisher<Bool, Never> in
-                    guard let self = self else {
-                        return Just(false).eraseToAnyPublisher()
+            Task {
+                do {
+                    if let result = await self.apsManager.autotune() {
+                        autotune = result
+                        _ = try await self.apsManager.makeProfiles()
+                        lastAutotuneDate = Date()
                     }
-                    self.autotune = result
-                    return self.apsManager.makeProfiles()
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to run Autotune")
                 }
-                .sink { [weak self] _ in
-                    self?.lastAutotuneDate = Date()
-                }.store(in: &lifetime)
+            }
         }
 
-        func delete() {
+        func delete() async {
             provider.deleteAutotune()
             autotune = nil
-            apsManager.makeProfiles()
-                .cancellable()
-                .store(in: &lifetime)
+            do {
+                _ = try await apsManager.makeProfiles()
+            } catch {
+                return
+            }
         }
 
         func replace() {

+ 5 - 1
FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift

@@ -109,7 +109,11 @@ extension AutotuneConfig {
                     }
 
                     Section {
-                        Button { state.delete() }
+                        Button {
+                            Task {
+                                await state.delete()
+                            }
+                        }
                         label: { Text("Delete autotune data") }
                             .foregroundColor(.red)
                     }

+ 15 - 16
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -124,16 +124,14 @@ extension Bolus {
             useFPUconversion = settingsManager.settings.useFPUconversion
 
             if waitForSuggestionInitial {
-                apsManager.determineBasal()
-                    .receive(on: DispatchQueue.main)
-                    .sink { [weak self] ok in
-                        guard let self = self else { return }
-                        if !ok {
-                            self.waitForSuggestion = false
-                            self.insulinRequired = 0
-                            self.insulinRecommended = 0
-                        }
-                    }.store(in: &lifetime)
+                Task {
+                    let ok = await apsManager.determineBasal()
+                    if !ok {
+                        self.waitForSuggestion = false
+                        self.insulinRequired = 0
+                        self.insulinRecommended = 0
+                    }
+                }
             }
         }
 
@@ -361,13 +359,12 @@ extension Bolus {
                     )
                 ]
             )
-            debug(.default, "External insulin saved to pumphistory.json")
+            debug(.default, "External insulin saved")
 
             // save to core data asynchronously
             context.perform {
                 // create pump event
                 let newPumpEvent = PumpEventStored(context: self.context)
-//                newPumpEvent.id = UUID().uuidString
                 newPumpEvent.timestamp = self.date
                 newPumpEvent.type = PumpEvent.bolus.rawValue
 
@@ -387,13 +384,13 @@ extension Bolus {
             }
 
             // perform determine basal sync
-            apsManager.determineBasalSync()
+            Task {
+                await apsManager.determineBasalSync()
+            }
         }
 
         // MARK: - Carbs
 
-        // we need to also fetch the data after we have saved them in order to update the array and the UI because of the MVVM Architecture
-
         func saveMeal() {
             guard carbs > 0 || fat > 0 || protein > 0 else { return }
             carbs = min(carbs, maxCarbs)
@@ -415,7 +412,9 @@ extension Bolus {
             if carbs > 0 {
                 // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
                 if amount <= 0 {
-                    apsManager.determineBasalSync()
+                    Task {
+                        await apsManager.determineBasalSync()
+                    }
                 }
             }
         }

+ 4 - 21
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -138,7 +138,7 @@ extension DataTable {
             // Delete carbs also from Nightscout and perform a determine basal sync to update cob
             if let carbEntry = carbEntry {
                 provider.deleteCarbs(carbEntry)
-                apsManager.determineBasalSync()
+                await apsManager.determineBasalSync()
             }
         }
 
@@ -160,7 +160,9 @@ extension DataTable {
                     CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
                     provider.deleteInsulin(with: treatmentObjectID)
-                    apsManager.determineBasalSync()
+
+                    await apsManager.determineBasalSync()
+
                 } else {
                     print("authentication failed")
                 }
@@ -172,25 +174,6 @@ extension DataTable {
         func addManualGlucose() {
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let glucoseAsInt = Int(glucose)
-            let now = Date()
-            let id = UUID().uuidString
-
-            let saveToJSON = BloodGlucose(
-                _id: id,
-                direction: nil,
-                date: Decimal(now.timeIntervalSince1970) * 1000,
-                dateString: now,
-                unfiltered: nil,
-                filtered: nil,
-                noise: nil,
-                glucose: Int(glucose),
-                type: GlucoseType.manual.rawValue
-            )
-
-            // TODO: -do we need this?
-            // Save to Health
-            var saveToHealth = [BloodGlucose]()
-//            saveToHealth.append(saveToJSON)
 
             // save to core data
             coredataContext.perform {

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -216,7 +216,7 @@ extension Home {
                 await apsManager.cancelBolus()
 
                 // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
-                apsManager.determineBasalSync()
+                await apsManager.determineBasalSync()
             }
         }
 

+ 5 - 3
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -114,9 +114,11 @@ import UIKit
     /// attempts to present this live activity state, creating a new activity if none exists yet
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
 //        // End all activities that are not the current one
-//        for unknownActivity in Activity<LiveActivityAttributes>.activities.filter({ self.currentActivity?.activity.id != $0.id }) {
-//            await unknownActivity.end(nil, dismissalPolicy: .immediate)
-//        }
+        for unknownActivity in Activity<LiveActivityAttributes>.activities
+            .filter({ self.currentActivity?.activity.id != $0.id })
+        {
+            await unknownActivity.end(nil, dismissalPolicy: .immediate)
+        }
 
         if let currentActivity = currentActivity {
             if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {

+ 5 - 5
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -452,7 +452,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
                     sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
                     rate: lastDetermination.rate?.decimalValue,
-                    duration: Int(lastDetermination.duration),
+                    duration: lastDetermination.duration?.decimalValue,
                     iob: lastDetermination.iob?.decimalValue,
                     cob: Decimal(lastDetermination.cob),
                     predictions: nil,
@@ -486,7 +486,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
                     sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
                     rate: lastDetermination.rate?.decimalValue,
-                    duration: Int(lastDetermination.duration),
+                    duration: lastDetermination.duration?.decimalValue,
                     iob: lastDetermination.iob?.decimalValue,
                     cob: Decimal(lastDetermination.cob),
                     predictions: nil,
@@ -521,7 +521,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
                     sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
                     rate: lastDetermination.rate?.decimalValue,
-                    duration: Int(lastDetermination.duration),
+                    duration: lastDetermination.duration?.decimalValue,
                     iob: lastDetermination.iob?.decimalValue,
                     cob: Decimal(lastDetermination.cob),
                     predictions: nil,
@@ -555,7 +555,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     eventualBG: Int(truncating: penultimateDetermination.eventualBG ?? 0),
                     sensitivityRatio: penultimateDetermination.sensitivityRatio?.decimalValue,
                     rate: penultimateDetermination.rate?.decimalValue,
-                    duration: Int(penultimateDetermination.duration),
+                    duration: lastDetermination.duration?.decimalValue,
                     iob: penultimateDetermination.iob?.decimalValue,
                     cob: Decimal(penultimateDetermination.cob),
                     predictions: nil,
@@ -592,7 +592,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     eventualBG: Int(truncating: lastDetermination.eventualBG ?? 0),
                     sensitivityRatio: lastDetermination.sensitivityRatio?.decimalValue,
                     rate: lastDetermination.rate?.decimalValue,
-                    duration: Int(lastDetermination.duration),
+                    duration: lastDetermination.duration?.decimalValue,
                     iob: lastDetermination.iob?.decimalValue,
                     cob: Decimal(lastDetermination.cob),
                     predictions: nil,

+ 13 - 13
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -375,24 +375,24 @@ extension BaseWatchManager: WCSessionDelegate {
                     actualDate: nil,
                     carbs: Decimal(carbs),
                     fat: Decimal(fat),
-                    protein: Decimal(protein), note: nil,
+                    protein: Decimal(protein),
+                    note: nil,
                     enteredBy: CarbsEntry.manual,
-                    isFPU: false, fpuID: nil
+                    isFPU: false,
+                    fpuID: nil
                 )]
             )
 
-            if settingsManager.settings.skipBolusScreenAfterCarbs {
-                apsManager.determineBasalSync()
-                replyHandler(["confirmation": true])
-                return
-            } else {
-                apsManager.determineBasal()
-                    .sink { _ in
-                        replyHandler(["confirmation": true])
-                    }
-                    .store(in: &lifetime)
-                return
+            Task {
+                if settingsManager.settings.skipBolusScreenAfterCarbs {
+                    let success = await apsManager.determineBasal()
+                    replyHandler(["confirmation": success])
+                } else {
+                    _ = await apsManager.determineBasal()
+                    replyHandler(["confirmation": true])
+                }
             }
+            return
         }
 
         if let tempTargetID = message["tempTarget"] as? String {

+ 18 - 92
Model/CoreDataStack.swift

@@ -278,80 +278,10 @@ extension CoreDataStack {
         return result ?? []
     }
 
-    // TODO: -refactor this, currently only the BolusStateModel uses this because we need to fetch in the background, then do calculations and after this update the UI
-
-    // Fetch and update UI
-    /// - Tag: uiFetch
-    func fetchEntitiesAndUpdateUI<T: NSManagedObject>(
-        ofType type: T.Type,
-        predicate: NSPredicate,
-        key: String,
-        ascending: Bool,
-        fetchLimit: Int? = nil,
-        batchSize: Int? = nil,
-        propertiesToFetch: [String]? = nil,
-        callingFunction: String = #function,
-        callingClass: String = #fileID,
-        completion: @escaping ([T]) -> Void
-    ) {
-        let request = NSFetchRequest<NSManagedObjectID>(entityName: String(describing: type))
-        request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
-        request.predicate = predicate
-        request.resultType = .managedObjectIDResultType
-        if let limit = fetchLimit {
-            request.fetchLimit = limit
-        }
-        if let batchSize = batchSize {
-            request.fetchBatchSize = batchSize
-        }
-        if let propertiesToFetch = propertiesToFetch {
-            request.propertiesToFetch = propertiesToFetch
-        }
-
-        let taskContext = newTaskContext()
-        taskContext.name = "fetchContext"
-        taskContext.transactionAuthor = "fetchEntities"
-
-        // perform fetch in the background
-        //
-        // the fetch returns a NSManagedObjectID which can be safely passed to the main queue because they are thread safe
-        taskContext.perform {
-            var result: [NSManagedObjectID]?
-
-            do {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on thread \(Thread.current)"
-                )
-                result = try taskContext.fetch(request)
-            } catch let error as NSError {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on thread \(Thread.current)"
-                )
-            }
-
-            // change to the main queue to update UI
-            DispatchQueue.main.async {
-                if let result = result {
-                    debugPrint(
-                        "Returning fetch result to main thread in \(callingFunction) from \(callingClass) on thread \(Thread.current)"
-                    )
-                    // Convert NSManagedObjectIDs to objects in the main context
-                    let mainContext = CoreDataStack.shared.persistentContainer.viewContext
-                    let mainContextObjects = result.compactMap { mainContext.object(with: $0) as? T }
-                    completion(mainContextObjects)
-                } else {
-                    debugPrint("Fetch result is nil in \(callingFunction) from \(callingClass) on thread \(Thread.current)")
-                    completion([])
-                }
-            }
-        }
-    }
-
-    // Fetch the NSManagedObjectIDs
-    // Useful if we need to pass the NSManagedObject to another thread as the objectID is thread safe
-    /// - Tag: fetchIDs
-    func fetchNSManagedObjectID<T: NSManagedObject>(
+    // Fetch Async
+    func fetchEntitiesAsync<T: NSManagedObject>(
         ofType type: T.Type,
+        onContext context: NSManagedObjectContext,
         predicate: NSPredicate,
         key: String,
         ascending: Bool,
@@ -359,43 +289,39 @@ extension CoreDataStack {
         batchSize: Int? = nil,
         propertiesToFetch: [String]? = nil,
         callingFunction: String = #function,
-        callingClass: String = #fileID,
-        completion: @escaping ([NSManagedObjectID]) -> Void
-    ) {
-        let request = NSFetchRequest<NSManagedObjectID>(entityName: String(describing: type))
+        callingClass: String = #fileID
+    ) async -> [T] {
+        let request = NSFetchRequest<T>(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
-        request.resultType = .managedObjectIDResultType
         if let limit = fetchLimit {
             request.fetchLimit = limit
         }
         if let batchSize = batchSize {
             request.fetchBatchSize = batchSize
         }
-        if let propertiesToFetch = propertiesToFetch {
-            request.propertiesToFetch = propertiesToFetch
+        if let propertiesTofetch = propertiesToFetch {
+            request.propertiesToFetch = propertiesTofetch
+            request.resultType = .managedObjectResultType
+        } else {
+            request.resultType = .managedObjectResultType
         }
 
-        let taskContext = newTaskContext()
-        taskContext.name = "fetchContext"
-        taskContext.transactionAuthor = "fetchEntities"
-
-        // Perform fetch in the background
-        taskContext.perform {
-            var result: [NSManagedObjectID]?
+        context.name = "fetchContext"
+        context.transactionAuthor = "fetchEntities"
 
+        return await context.perform {
             do {
                 debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on thread \(Thread.current)"
+                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
                 )
-                result = try taskContext.fetch(request)
+                return try context.fetch(request)
             } catch let error as NSError {
                 debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on thread \(Thread.current)"
+                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
                 )
+                return []
             }
-
-            completion(result ?? [])
         }
     }
 }

+ 2 - 2
Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E224" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G120" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -78,7 +78,7 @@
         <attribute name="cob" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="currentTarget" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="deliverAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-        <attribute name="duration" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="duration" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="enacted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="eventualBG" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="expectedDelta" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>

+ 1 - 0
Model/Helper/CustomNotification.swift

@@ -4,4 +4,5 @@ extension Notification.Name {
     static let didPerformBatchInsert = Notification.Name("didPerformBatchInsert")
     static let didPerformBatchUpdate = Notification.Name("didPerformBatchUpdate")
     static let didPerformBatchDelete = Notification.Name("didPerformBatchDelete")
+    static let didUpdateDetermination = Notification.Name("didUpdateDetermination")
 }

+ 1 - 1
OrefDetermination+CoreDataProperties.swift

@@ -12,7 +12,7 @@ public extension OrefDetermination {
     @NSManaged var cob: Int16
     @NSManaged var currentTarget: NSDecimalNumber?
     @NSManaged var deliverAt: Date?
-    @NSManaged var duration: Int16
+    @NSManaged var duration: NSDecimalNumber?
     @NSManaged var enacted: Bool
     @NSManaged var eventualBG: NSDecimalNumber?
     @NSManaged var expectedDelta: NSDecimalNumber?