Bläddra i källkod

Use Core data to draw temp targets WiP

polscm32 1 år sedan
förälder
incheckning
8f6a88aeda

+ 20 - 12
FreeAPS/Sources/Helpers/MainChartHelper.swift

@@ -67,31 +67,39 @@ enum MainChartHelper {
         units == .mgdL ? 30 : 1.66
     }
 
-    static func calculateDuration(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> TimeInterval? {
+    static func calculateDuration(
+        objectID: NSManagedObjectID,
+        attribute: String,
+        context: NSManagedObjectContext
+    ) -> TimeInterval? {
         do {
-            if let override = try context.existingObject(with: objectID) as? OverrideStored,
-               let overrideDuration = override.duration as? Double, overrideDuration != 0
-            {
-                return TimeInterval(overrideDuration * 60) // return seconds
+            let object = try context.existingObject(with: objectID)
+            if let attributeValue = object.value(forKey: attribute) as? NSDecimalNumber {
+                let doubleValue = attributeValue.doubleValue
+                if doubleValue != 0 {
+                    return TimeInterval(doubleValue * 60) // return seconds
+                }
+            } else {
+                debugPrint("Attribute \(attribute) not found or not of type NSDecimalNumber")
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate duration for object with error: \(error.localizedDescription)"
             )
         }
+
         return nil
     }
 
-    static func calculateTarget(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> Decimal? {
+    static func calculateTarget(objectID: NSManagedObjectID, attribute: String, context: NSManagedObjectContext) -> Decimal? {
         do {
-            if let override = try context.existingObject(with: objectID) as? OverrideStored,
-               let overrideTarget = override.target, overrideTarget != 0
-            {
-                return overrideTarget.decimalValue
+            let object = try context.existingObject(with: objectID)
+            if let attributeValue = object.value(forKey: attribute) as? NSDecimalNumber, attributeValue != 0 {
+                return attributeValue.decimalValue
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate target for object with error: \(error.localizedDescription)"
             )
         }
         return nil

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

@@ -76,6 +76,8 @@ extension Home {
         @Published var lastPumpBolus: PumpEventStored?
         @Published var overrides: [OverrideStored] = []
         @Published var overrideRunStored: [OverrideRunStored] = []
+        @Published var tempTargetStored: [TempTargetStored] = []
+        @Published var tempTargetRunStored: [TempTargetRunStored] = []
         @Published var isOverrideCancelled: Bool = false
         @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         @Published var pumpStatusHighlightMessage: String? = nil
@@ -157,6 +159,12 @@ extension Home {
                         self.setupOverrideRunStored()
                     }
                     group.addTask {
+                        self.setupTempTargetsStored()
+                    }
+                    group.addTask {
+                        self.setupTempTargetsRunStored()
+                    }
+                    group.addTask {
                         await self.setupSettings()
                     }
                     group.addTask {
@@ -936,6 +944,67 @@ extension Home.StateModel {
         overrideRunStored = objects
     }
 
+    // Setup active TempTargets
+    private func setupTempTargetsStored() {
+        Task {
+            let ids = await self.fetchTempTargets()
+            let tempTargetObjects: [TempTargetStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateTempTargetsArray(with: tempTargetObjects)
+        }
+    }
+
+    private func fetchTempTargets() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastActiveTempTarget,
+            key: "date",
+            ascending: false
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await context.perform {
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateTempTargetsArray(with objects: [TempTargetStored]) {
+        tempTargetStored = objects
+    }
+
+    // Setup expired TempTargets
+    private func setupTempTargetsRunStored() {
+        Task {
+            let ids = await self.fetchTempTargetRunStored()
+            let tempTargetRunObjects: [TempTargetRunStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+        }
+    }
+
+    private func fetchTempTargetRunStored() async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetRunStored.self,
+            onContext: context,
+            predicate: predicate,
+            key: "startDate",
+            ascending: false
+        )
+
+        guard let fetchedResults = results as? [TempTargetRunStored] else { return [] }
+
+        return await context.perform {
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateTempTargetRunStoredArray(with objects: [TempTargetRunStored]) {
+        tempTargetRunStored = objects
+    }
+
     @MainActor func saveToTempTargetRunStored(withID id: NSManagedObjectID) async {
         await viewContext.perform {
             do {

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

@@ -21,7 +21,7 @@ struct MainChartView: View {
 
     @State var basalProfiles: [BasalProfile] = []
     @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
-    @State var chartTempTargets: [ChartTempTarget] = []
+//    @State var chartTempTargets: [ChartTempTarget] = []
     @State var startMarker =
         Date(timeIntervalSinceNow: TimeInterval(hours: -24))
     @State var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
@@ -145,7 +145,6 @@ extension MainChartView {
                 drawStartRuleMark()
                 drawEndRuleMark()
                 drawCurrentTimeMarker()
-                drawTempTargets()
 
                 GlucoseChartView(
                     glucoseData: state.glucoseFromPersistence,
@@ -176,6 +175,13 @@ extension MainChartView {
                     viewContext: context
                 )
 
+                TempTargetView(
+                    tempTargetStored: state.tempTargetStored,
+                    tempTargetRunStored: state.tempTargetRunStored,
+                    units: state.units,
+                    viewContext: context
+                )
+
                 ForecastView(
                     preprocessedData: state.preprocessedData,
                     minForecast: state.minForecast,
@@ -229,11 +235,6 @@ extension MainChartView {
             .onChange(of: state.insulinFromPersistence) { _ in
                 state.roundedTotalBolus = state.calculateTINS()
             }
-            .onChange(of: tempTargets) { _ in
-                Task {
-                    await calculateTempTargets()
-                }
-            }
             .frame(minHeight: geo.size.height * 0.28)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)

+ 10 - 2
FreeAPS/Sources/Modules/Home/View/Chart/OverrideView.swift

@@ -16,11 +16,19 @@ struct OverrideView: ChartContent {
 
     private func drawActiveOverrides() -> some ChartContent {
         ForEach(overrides) { override in
-            if let duration = MainChartHelper.calculateDuration(objectID: override.objectID, context: viewContext) {
+            if let duration = MainChartHelper.calculateDuration(
+                objectID: override.objectID,
+                attribute: "duration",
+                context: viewContext
+            ) {
                 let start: Date = override.date ?? .distantPast
                 let end: Date = start.addingTimeInterval(duration)
 
-                if let target = MainChartHelper.calculateTarget(objectID: override.objectID, context: viewContext) {
+                if let target = MainChartHelper.calculateTarget(
+                    objectID: override.objectID,
+                    attribute: "target",
+                    context: viewContext
+                ) {
                     RuleMark(
                         xStart: .value("Start", start, unit: .second),
                         xEnd: .value("End", end, unit: .second),

+ 43 - 69
FreeAPS/Sources/Modules/Home/View/Chart/TempTargets.swift

@@ -1,82 +1,56 @@
 import Charts
+import CoreData
 import Foundation
 import SwiftUI
 
-struct ChartTempTarget: Hashable {
-    let amount: Decimal
-    let start: Date
-    let end: Date
-}
-
-extension MainChartView {
-    func drawTempTargets() -> some ChartContent {
-        ForEach(chartTempTargets, id: \.self) { target in
-            let targetLimited = min(max(target.amount, 0), upperLimit)
+struct TempTargetView: ChartContent {
+    let tempTargetStored: [TempTargetStored]
+    let tempTargetRunStored: [TempTargetRunStored]
+    let units: GlucoseUnits
+    let viewContext: NSManagedObjectContext
 
-            RuleMark(
-                xStart: .value("Start", target.start),
-                xEnd: .value("End", target.end),
-                y: .value("Value", targetLimited)
-            )
-            .foregroundStyle(Color.green.opacity(0.75)).lineStyle(.init(lineWidth: 8))
-        }
+    var body: some ChartContent {
+        drawActiveTempTargets()
+        drawTempTargetRunStored()
     }
 
-    // Calculations for temp target bar mark
-    func calculateTempTargets() async {
-        // Perform calculations off the main thread
-        let calculatedTTs = await Task.detached { () -> [ChartTempTarget] in
-            var groupedPackages: [[TempTarget]] = []
-            var currentPackage: [TempTarget] = []
-            var calculatedTTs: [ChartTempTarget] = []
-
-            for target in await tempTargets {
-                if target.duration > 0 {
-                    if !currentPackage.isEmpty {
-                        groupedPackages.append(currentPackage)
-                        currentPackage = []
-                    }
-                    currentPackage.append(target)
-                } else if let lastNonZeroTempTarget = currentPackage.last(where: { $0.duration > 0 }) {
-                    // Ensure this cancel target is within the valid time range
-                    if target.createdAt >= lastNonZeroTempTarget.createdAt,
-                       target.createdAt <= lastNonZeroTempTarget.createdAt
-                       .addingTimeInterval(TimeInterval(lastNonZeroTempTarget.duration * 60))
-                    {
-                        currentPackage.append(target)
-                    }
+    private func drawActiveTempTargets() -> some ChartContent {
+        ForEach(tempTargetStored) { tt in
+            if let duration = MainChartHelper.calculateDuration(
+                objectID: tt.objectID,
+                attribute: "duration",
+                context: viewContext
+            ) {
+                let start: Date = tt.date ?? .distantPast
+                let end: Date = start.addingTimeInterval(duration)
+
+                if let target = MainChartHelper
+                    .calculateTarget(objectID: tt.objectID, attribute: "target", context: viewContext)
+                {
+                    RuleMark(
+                        xStart: .value("Start", start, unit: .second),
+                        xEnd: .value("End", end, unit: .second),
+                        y: .value("Value", units == .mgdL ? target : target.asMmolL)
+                    )
+                    .foregroundStyle(Color.green.opacity(0.4))
+                    .lineStyle(.init(lineWidth: 8))
                 }
             }
+        }
+    }
 
-            // Append the last group, if any
-            if !currentPackage.isEmpty {
-                groupedPackages.append(currentPackage)
-            }
-
-            for package in groupedPackages {
-                guard let firstNonZeroTarget = package.first(where: { $0.duration > 0 }) else { continue }
-
-                var end = firstNonZeroTarget.createdAt.addingTimeInterval(TimeInterval(firstNonZeroTarget.duration * 60))
-
-                let earliestCancelTarget = package.filter({ $0.duration == 0 }).min(by: { $0.createdAt < $1.createdAt })
-
-                if let earliestCancelTarget = earliestCancelTarget {
-                    end = min(earliestCancelTarget.createdAt, end)
-                }
-
-                if let targetTop = firstNonZeroTarget.targetTop {
-                    let adjustedTarget = await units == .mgdL ? targetTop : targetTop.asMmolL
-                    calculatedTTs
-                        .append(ChartTempTarget(amount: adjustedTarget, start: firstNonZeroTarget.createdAt, end: end))
-                }
-            }
-
-            return calculatedTTs
-        }.value
-
-        // Update chartTempTargets on the main thread
-        await MainActor.run {
-            self.chartTempTargets = calculatedTTs
+    private func drawTempTargetRunStored() -> some ChartContent {
+        ForEach(tempTargetRunStored) { tt in
+            let start: Date = tt.startDate ?? .distantPast
+            let end: Date = tt.endDate ?? Date()
+            let target = tt.target?.decimalValue ?? 100
+            RuleMark(
+                xStart: .value("Start", start, unit: .second),
+                xEnd: .value("End", end, unit: .second),
+                y: .value("Value", units == .mgdL ? target : target.asMmolL)
+            )
+            .foregroundStyle(Color.green.opacity(0.25))
+            .lineStyle(.init(lineWidth: 8))
         }
     }
 }

+ 0 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -524,7 +524,6 @@ extension Home {
 
         @ViewBuilder func adjustmentsTempTargetView(_ tempTargetString: String) -> some View {
             Group {
-                /// TempTarget section
                 Image(systemName: "target")
                     .font(.system(size: 24))
                     .foregroundColor(.loopGreen)

+ 20 - 20
FreeAPS/Sources/Modules/OverrideConfig/View/AddTempTargetForm.swift

@@ -100,29 +100,29 @@ struct AddTempTargetForm: View {
         Section(
             header: Text("Configure Temp Target"),
             content: {
-            HStack {
-                Text("Name")
-                Spacer()
-                TextField("Enter Name (optional)", text: $state.tempTargetName)
-                    .multilineTextAlignment(.trailing)
-            }
+                HStack {
+                    Text("Name")
+                    Spacer()
+                    TextField("Enter Name (optional)", text: $state.tempTargetName)
+                        .multilineTextAlignment(.trailing)
+                }
 
-            HStack {
-                Text("Target")
-                Spacer()
-                TextFieldWithToolBar(text: $state.tempTargetTarget, placeholder: "0", numberFormatter: glucoseFormatter)
-                Text(state.units.rawValue).foregroundColor(.secondary)
-            }
+                HStack {
+                    Text("Target")
+                    Spacer()
+                    TextFieldWithToolBar(text: $state.tempTargetTarget, placeholder: "0", numberFormatter: glucoseFormatter)
+                    Text(state.units.rawValue).foregroundColor(.secondary)
+                }
 
-            HStack {
-                Text("Duration")
-                Spacer()
-                TextFieldWithToolBar(text: $state.tempTargetDuration, placeholder: "0", numberFormatter: formatter)
-                Text("minutes").foregroundColor(.secondary)
+                HStack {
+                    Text("Duration")
+                    Spacer()
+                    TextFieldWithToolBar(text: $state.tempTargetDuration, placeholder: "0", numberFormatter: formatter)
+                    Text("minutes").foregroundColor(.secondary)
+                }
+                DatePicker("Date", selection: $state.date)
             }
-            DatePicker("Date", selection: $state.date)
-        }
-                ).listRowBackground(Color.chart)
+        ).listRowBackground(Color.chart)
 
         // TODO: with iOS 17 we can change the body content wrapper from FORM to LIST and apply the .listSpacing modifier to make this all nice and small.
         Section {

+ 42 - 42
FreeAPS/Sources/Modules/OverrideConfig/View/EditTempTargetForm.swift

@@ -116,49 +116,49 @@ struct EditTempTargetForm: View {
     @ViewBuilder private func editTempTarget() -> some View {
         Section(
             header: Text("Configure Temp Target"),
-        content: {
-        HStack {
-            Text("Name")
-            Spacer()
-            TextField("Enter Name (optional)", text: $name)
-                .multilineTextAlignment(.trailing)
-        }
-            HStack {
-                Text("Target")
-                Spacer()
-                TextFieldWithToolBar(
-                    text: Binding(
-                        get: { target },
-                        set: {
-                            target = $0
-                            hasChanges = true
-                        }
-                    ),
-                    placeholder: "0",
-                    numberFormatter: glucoseFormatter
-                )
-                Text(state.units.rawValue).foregroundColor(.secondary)
-            }
-            HStack {
-                Text("Duration")
-                Spacer()
-                TextFieldWithToolBar(
-                    text: Binding(
-                        get: { duration },
-                        set: {
-                            duration = $0
-                            hasChanges = true
-                        }
-                    ),
-                    placeholder: "0",
-                    numberFormatter: formatter
-                )
-                Text("minutes").foregroundColor(.secondary)
+            content: {
+                HStack {
+                    Text("Name")
+                    Spacer()
+                    TextField("Enter Name (optional)", text: $name)
+                        .multilineTextAlignment(.trailing)
+                }
+                HStack {
+                    Text("Target")
+                    Spacer()
+                    TextFieldWithToolBar(
+                        text: Binding(
+                            get: { target },
+                            set: {
+                                target = $0
+                                hasChanges = true
+                            }
+                        ),
+                        placeholder: "0",
+                        numberFormatter: glucoseFormatter
+                    )
+                    Text(state.units.rawValue).foregroundColor(.secondary)
+                }
+                HStack {
+                    Text("Duration")
+                    Spacer()
+                    TextFieldWithToolBar(
+                        text: Binding(
+                            get: { duration },
+                            set: {
+                                duration = $0
+                                hasChanges = true
+                            }
+                        ),
+                        placeholder: "0",
+                        numberFormatter: formatter
+                    )
+                    Text("minutes").foregroundColor(.secondary)
+                }
+                DatePicker("Date", selection: $date)
+                    .onChange(of: date) { _ in hasChanges = true }
             }
-            DatePicker("Date", selection: $date)
-                .onChange(of: date) { _ in hasChanges = true }
-        }
-                ).listRowBackground(Color.chart)
+        ).listRowBackground(Color.chart)
 
         if state.computeSliderLow() != state.computeSliderHigh() {
             Section {