فهرست منبع

refactoring

fix duplicate NS entries

fix duration update of uploaded overrides

fix cancelling of multiple overrides from home view

fix logic in saveCustomOverride function

use async let for better performance

fix superbolus calculation

fix TINS calculation

Co-Authored-By: polscm32 aka Marvout <marvin-polscheit@t-online.de>
polscm32 aka Marvout 1 سال پیش
والد
کامیت
7c09c94eb4

+ 5 - 3
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -169,7 +169,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
         newOverride.smbIsOff = override.smbIsOff
         newOverride.name = override.name
         newOverride.isPreset = false // no Preset
-        newOverride.date = Date()
+        newOverride.date = override.date
         newOverride.enabled = override.enabled
         newOverride.target = override.target
         newOverride.advancedSettings = override.advancedSettings
@@ -220,7 +220,8 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
                     eventType: OverrideStored.EventType.nsExercise,
                     createdAt: override.date ?? Date(),
                     enteredBy: NightscoutExercise.local,
-                    notes: override.name ?? "Custom Override"
+                    notes: override.name ?? "Custom Override",
+                    id: UUID(uuidString: override.id ?? UUID().uuidString)
                 )
             }
         }
@@ -248,7 +249,8 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
                     eventType: OverrideStored.EventType.nsExercise,
                     createdAt: (overrideRun.startDate ?? overrideRun.override?.date) ?? Date(),
                     enteredBy: NightscoutExercise.local,
-                    notes: overrideRun.name ?? "Custom Override"
+                    notes: overrideRun.name ?? "Custom Override",
+                    id: overrideRun.id
                 )
             }
         }

+ 1 - 0
FreeAPS/Sources/Models/NightscoutExercise.swift

@@ -6,6 +6,7 @@ struct NightscoutExercise: JSON, Hashable, Equatable {
     var createdAt: Date
     var enteredBy: String?
     var notes: String?
+    var id: UUID?
 
     static let local = "Trio"
 

+ 23 - 7
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -140,25 +140,41 @@ extension Bolus {
         func getCurrentBasal() {
             let basalEntries = provider.getProfile()
             let now = Date()
+            let calendar = Calendar.current
             let dateFormatter = DateFormatter()
             dateFormatter.dateFormat = "HH:mm:ss"
+            dateFormatter.timeZone = TimeZone.current
 
-            // iterate over basal entries
             for (index, entry) in basalEntries.enumerated() {
-                guard let entryStartTime = dateFormatter.date(from: entry.start) else { continue }
+                guard let entryTime = dateFormatter.date(from: entry.start) else {
+                    print("Invalid entry start time: \(entry.start)")
+                    continue
+                }
+
+                // Combine the current date with the time from entry.start
+                let entryStartTime = calendar.date(
+                    bySettingHour: calendar.component(.hour, from: entryTime),
+                    minute: calendar.component(.minute, from: entryTime),
+                    second: calendar.component(.second, from: entryTime),
+                    of: now
+                )!
 
                 let entryEndTime: Date
                 if index < basalEntries.count - 1,
-                   let nextEntryStartTime = dateFormatter.date(from: basalEntries[index + 1].start)
+                   let nextEntryTime = dateFormatter.date(from: basalEntries[index + 1].start)
                 {
-                    // end of current entry should equal start of next entry
+                    let nextEntryStartTime = calendar.date(
+                        bySettingHour: calendar.component(.hour, from: nextEntryTime),
+                        minute: calendar.component(.minute, from: nextEntryTime),
+                        second: calendar.component(.second, from: nextEntryTime),
+                        of: now
+                    )!
                     entryEndTime = nextEntryStartTime
                 } else {
-                    // if it is the last entry use current time as end of entry
-                    entryEndTime = now
+                    // If it's the last entry, use the same start time plus one day as the end time
+                    entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
                 }
 
-                // proof if current time is between start and end of entry
                 if now >= entryStartTime, now < entryEndTime {
                     currentBasal = entry.rate
                     break

+ 47 - 14
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -72,7 +72,6 @@ extension Home {
         @Published var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
         @Published var insulinFromPersistence: [PumpEventStored] = []
         @Published var tempBasals: [PumpEventStored] = []
-        var boluses: [PumpEventStored] = []
         @Published var suspensions: [PumpEventStored] = []
         @Published var batteryFromPersistence: [OpenAPS_Battery] = []
         @Published var lastPumpBolus: PumpEventStored?
@@ -82,6 +81,8 @@ extension Home {
         let context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
+        typealias PumpEvent = PumpEventStored.EventType
+
         override func subscribe() {
             setupNotification()
             setupGlucoseArray()
@@ -224,7 +225,7 @@ extension Home {
             }
         }
 
-        @MainActor func cancelProfile(withID id: NSManagedObjectID) async {
+        @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
             do {
                 let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
                 profileToCancel?.enabled = false
@@ -233,25 +234,60 @@ extension Home {
 
                 guard viewContext.hasChanges else { return }
                 try viewContext.save()
+
+                Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
             } catch {
                 debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
             }
         }
 
         func calculateTINS() -> String {
+            let startTime = calculateStartTime(hours: Int(hours))
+
+            let totalBolus = calculateTotalBolus(from: insulinFromPersistence, since: startTime)
+            let totalBasal = calculateTotalBasal(from: insulinFromPersistence, since: startTime)
+
+            let totalInsulin = totalBolus + totalBasal
+
+            return formatInsulinAmount(totalInsulin)
+        }
+
+        private func calculateStartTime(hours: Int) -> Date {
             let date = Date()
             let calendar = Calendar.current
-            let offset = hours
-
             var offsetComponents = DateComponents()
-            offsetComponents.hour = -Int(offset)
-            let startTime = calendar.date(byAdding: offsetComponents, to: date)!
+            offsetComponents.hour = -hours
+            return calendar.date(byAdding: offsetComponents, to: date)!
+        }
 
-            let bolusesForCurrentDay = boluses.filter { $0.timestamp ?? .distantPast >= startTime }
-            let totalBolus = bolusesForCurrentDay.map { Double(truncating: $0.bolus?.amount ?? 0.0) }.reduce(0.0, +)
-            roundedTotalBolus = Decimal(round(100 * Double(totalBolus)) / 100).formatted()
+        private func calculateTotalBolus(from events: [PumpEventStored], since startTime: Date) -> Double {
+            let bolusEvents = events.filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.bolus.rawValue }
+            return bolusEvents.compactMap { $0.bolus?.amount?.doubleValue }.reduce(0, +)
+        }
+
+        private func calculateTotalBasal(from events: [PumpEventStored], since startTime: Date) -> Double {
+            let basalEvents = events
+                .filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.tempBasal.rawValue }
+                .sorted { $0.timestamp ?? .distantPast < $1.timestamp ?? .distantPast }
+
+            var basalDurations: [Double] = []
+            for (index, basalEntry) in basalEvents.enumerated() {
+                if index + 1 < basalEvents.count {
+                    let nextEntry = basalEvents[index + 1]
+                    let durationInSeconds = nextEntry.timestamp?.timeIntervalSince(basalEntry.timestamp ?? Date()) ?? 0
+                    basalDurations.append(durationInSeconds / 3600) // Conversion to hours
+                }
+            }
+
+            return zip(basalEvents, basalDurations).map { entry, duration in
+                guard let rate = entry.tempBasal?.rate?.doubleValue else { return 0 }
+                return rate * duration
+            }.reduce(0, +)
+        }
 
-            return roundedTotalBolus
+        private func formatInsulinAmount(_ amount: Double) -> String {
+            let roundedAmount = Decimal(round(100 * amount) / 100)
+            return roundedAmount.formatted()
         }
 
         private func setupPumpSettings() {
@@ -925,11 +961,8 @@ extension Home.StateModel {
                 newOverrideRunStored.override = object
                 newOverrideRunStored.isUploadedToNS = false
 
-                guard self.viewContext.hasChanges else { return }
-                try self.viewContext.save()
-
             } catch {
-                print(error.localizedDescription)
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
             }
         }
     }

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

@@ -238,7 +238,7 @@ extension MainChartView {
                 }
             }
             .id("MainChart")
-            .onChange(of: state.boluses) { _ in
+            .onChange(of: state.insulinFromPersistence) { _ in
                 state.roundedTotalBolus = state.calculateTINS()
             }
             .onChange(of: tempTargets) { _ in

+ 2 - 2
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -232,7 +232,7 @@ extension Home {
                     /// Do not show the Override anymore
                     Task {
                         guard let objectID = self.latestOverride.first?.objectID else { return }
-                        await state.cancelProfile(withID: objectID)
+                        await state.cancelOverride(withID: objectID)
                     }
                 }
             }
@@ -564,7 +564,7 @@ extension Home {
                             Button("Yes", role: .destructive) {
                                 Task {
                                     guard let objectID = latestOverride.first?.objectID else { return }
-                                    await state.cancelProfile(withID: objectID)
+                                    await state.cancelOverride(withID: objectID)
                                 }
                             }
                         }, message: { Text("This will change settings back to your normal profile.") }

+ 39 - 51
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -94,50 +94,18 @@ extension OverrideProfilesConfig {
 // MARK: - Setup Notifications
 
 extension OverrideProfilesConfig.StateModel {
-    /// listens for the notifications sent when the managedObjectContext has saved!
+    // Custom Notification to update View when an Override has been cancelled via Home View
     func setupNotification() {
         Foundation.NotificationCenter.default.addObserver(
             self,
-            selector: #selector(contextDidSave(_:)),
-            name: Notification.Name.NSManagedObjectContextDidSave,
+            selector: #selector(handleOverrideConfigurationUpdate),
+            name: .didUpdateOverrideConfiguration,
             object: nil
         )
-
-        /// listens for notifications sent when a Preset was added
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handlePresetsUpdate),
-            name: .didUpdateOverridePresets,
-            object: nil
-        )
-    }
-
-    @objc private func handlePresetsUpdate() {
-        setupOverridePresetsArray()
     }
 
-    /// determine the actions when the context has changed
-    /// its done on a background thread and after that the UI gets updated on the main thread
-    @objc private func contextDidSave(_ notification: Notification) {
-        guard let userInfo = notification.userInfo else { return }
-
-        Task { [weak self] in
-            await self?.processUpdates(userInfo: userInfo)
-        }
-    }
-
-    private func processUpdates(userInfo: [AnyHashable: Any]) async {
-        var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-        objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-        objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
-
-        let overrideUpdates = objects.filter { $0 is OverrideStored }
-
-        DispatchQueue.global(qos: .background).async {
-            if overrideUpdates.isNotEmpty {
-                self.updateLatestOverrideConfiguration()
-            }
-        }
+    @objc private func handleOverrideConfigurationUpdate() {
+        updateLatestOverrideConfiguration()
     }
 
     // MARK: - Enact Overrides
@@ -152,6 +120,9 @@ extension OverrideProfilesConfig.StateModel {
         do {
             guard viewContext.hasChanges else { return }
             try viewContext.save()
+
+            // Update Presets View
+            setupOverridePresetsArray()
         } catch {
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
@@ -166,6 +137,7 @@ extension OverrideProfilesConfig.StateModel {
             let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
             overrideToEnact?.enabled = true
             overrideToEnact?.date = Date()
+            overrideToEnact?.isUploadedToNS = false
 
             /// Update the 'Cancel Override' button state
             isEnabled = true
@@ -178,6 +150,9 @@ extension OverrideProfilesConfig.StateModel {
 
             guard viewContext.hasChanges else { return }
             try viewContext.save()
+
+            // Update View
+            updateLatestOverrideConfiguration()
         } catch {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
         }
@@ -225,6 +200,9 @@ extension OverrideProfilesConfig.StateModel {
                 // Save the context if there are changes
                 if self.viewContext.hasChanges {
                     try self.viewContext.save()
+
+                    // Update the View
+                    self.updateLatestOverrideConfiguration()
                 }
             } catch {
                 debugPrint(
@@ -262,12 +240,17 @@ extension OverrideProfilesConfig.StateModel {
             uamMinutes: uamMinutes
         )
 
-        if currentActiveOverride != nil {
-            await disableAllActiveOverrides(except: currentActiveOverride?.objectID, createOverrideRunEntry: true)
-        }
+        // First disable all Overrides
+        await disableAllActiveOverrides(createOverrideRunEntry: true)
+
+        // Then save and activate a new custom Override and reset the State variables
+        async let storeOverride: () = overrideStorage.storeOverride(override: override)
+        async let resetState: () = resetStateVariables()
 
-        await overrideStorage.storeOverride(override: override)
-        await resetStateVariables()
+        _ = await (storeOverride, resetState)
+
+        // Update View
+        updateLatestOverrideConfiguration()
     }
 
     // Save Presets
@@ -296,13 +279,13 @@ extension OverrideProfilesConfig.StateModel {
             uamMinutes: uamMinutes
         )
 
-        await overrideStorage.storeOverride(override: preset)
+        async let storeOverride: () = overrideStorage.storeOverride(override: preset)
+        async let resetState: () = resetStateVariables()
 
-        // Custom Notification to update Presets View
-        Foundation.NotificationCenter.default.post(name: .didUpdateOverridePresets, object: nil)
+        _ = await (storeOverride, resetState)
 
-        // Prevent showing the current config of the recently added Preset
-        await resetStateVariables()
+        // Update Presets View
+        setupOverridePresetsArray()
     }
 
     // MARK: - Setup Override Presets Array
@@ -332,8 +315,9 @@ extension OverrideProfilesConfig.StateModel {
 
     func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
         await overrideStorage.deleteOverridePreset(objectID)
-        // Custom Notification to update Presets View
-        Foundation.NotificationCenter.default.post(name: .didUpdateOverridePresets, object: nil)
+
+        // Update Presets View
+        setupOverridePresetsArray()
     }
 
     // MARK: - Setup the State variables with the last Override configuration
@@ -344,8 +328,10 @@ extension OverrideProfilesConfig.StateModel {
     func updateLatestOverrideConfiguration() {
         Task {
             let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
-            await updateLatestOverrideConfigurationOfState(from: id)
-            await setCurrentOverride(from: id)
+            async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
+            async let setOverride: () = setCurrentOverride(from: id)
+
+            _ = await (updateState, setOverride)
         }
     }
 
@@ -403,6 +389,8 @@ extension OverrideProfilesConfig.StateModel {
                 try self.viewContext.save()
             }
 
+            // Update View
+            // TODO: -
             if let overrideToEdit = try viewContext.existingObject(with: duplidateId) as? OverrideStored
             {
                 currentActiveOverride = overrideToEdit

+ 4 - 1
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/EditOverrideForm.swift

@@ -299,10 +299,13 @@ struct EditOverrideForm: View {
                             Task {
                                 await state.disableAllActiveOverrides(
                                     except: currentActiveOverride.objectID,
-                                    createOverrideRunEntry: true
+                                    createOverrideRunEntry: false
                                 )
                             }
                         }
+
+                        // Update View
+                        state.updateLatestOverrideConfiguration()
                         hasChanges = false
                         presentationMode.wrappedValue.dismiss()
                     } catch {

+ 1 - 1
Model/Helper/CustomNotification.swift

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