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

Merge branch 'testing' into core-data

polscm32 2 лет назад
Родитель
Сommit
f2906992b8

+ 4 - 0
Forecast+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(Forecast) public class Forecast: NSManagedObject {}

+ 32 - 0
Forecast+CoreDataProperties.swift

@@ -0,0 +1,32 @@
+import CoreData
+import Foundation
+
+public extension Forecast {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<Forecast> {
+        NSFetchRequest<Forecast>(entityName: "Forecast")
+    }
+
+    @NSManaged var date: Date?
+    @NSManaged var id: UUID?
+    @NSManaged var type: String?
+    @NSManaged var forecastValues: Set<ForecastValue>?
+    @NSManaged var orefDetermination: OrefDetermination?
+}
+
+// MARK: Generated accessors for forecastValues
+
+public extension Forecast {
+    @objc(addForecastValuesObject:)
+    @NSManaged func addToForecastValues(_ value: ForecastValue)
+
+    @objc(removeForecastValuesObject:)
+    @NSManaged func removeFromForecastValues(_ value: ForecastValue)
+
+    @objc(addForecastValues:)
+    @NSManaged func addToForecastValues(_ values: NSSet)
+
+    @objc(removeForecastValues:)
+    @NSManaged func removeFromForecastValues(_ values: NSSet)
+}
+
+extension Forecast: Identifiable {}

+ 4 - 0
ForecastValue+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(ForecastValue) public class ForecastValue: NSManagedObject {}

+ 14 - 0
ForecastValue+CoreDataProperties.swift

@@ -0,0 +1,14 @@
+import CoreData
+import Foundation
+
+public extension ForecastValue {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<ForecastValue> {
+        NSFetchRequest<ForecastValue>(entityName: "ForecastValue")
+    }
+
+    @NSManaged var index: Int32
+    @NSManaged var value: Int32
+    @NSManaged var forecast: Forecast?
+}
+
+extension ForecastValue: Identifiable {}

+ 20 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -384,6 +384,11 @@
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC41E29A2B1E1F460070974F /* HistoryLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC41E2992B1E1F460070974F /* HistoryLayout.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
+		CC76E94C2BD471BA008BEB61 /* Forecast+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC76E9482BD471BA008BEB61 /* Forecast+CoreDataClass.swift */; };
+		CC76E94D2BD471BA008BEB61 /* Forecast+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC76E9492BD471BA008BEB61 /* Forecast+CoreDataProperties.swift */; };
+		CC76E94E2BD471BA008BEB61 /* ForecastValue+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC76E94A2BD471BA008BEB61 /* ForecastValue+CoreDataClass.swift */; };
+		CC76E94F2BD471BA008BEB61 /* ForecastValue+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC76E94B2BD471BA008BEB61 /* ForecastValue+CoreDataProperties.swift */; };
+		CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */; };
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
 		CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */; };
 		CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1856F62ADC4869007E39C7 /* CarbPresetIntentRequest.swift */; };
@@ -999,6 +1004,11 @@
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CC41E2992B1E1F460070974F /* HistoryLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryLayout.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
+		CC76E9482BD471BA008BEB61 /* Forecast+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		CC76E9492BD471BA008BEB61 /* Forecast+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		CC76E94A2BD471BA008BEB61 /* ForecastValue+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ForecastValue+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		CC76E94B2BD471BA008BEB61 /* ForecastValue+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ForecastValue+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+helper.swift"; sourceTree = "<group>"; };
 		CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCarbPresetIntent.swift; sourceTree = "<group>"; };
 		CE1856F62ADC4869007E39C7 /* CarbPresetIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbPresetIntentRequest.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
@@ -2138,6 +2148,10 @@
 		5825D1052BD4056700F36E9B /* Classes+Properties */ = {
 			isa = PBXGroup;
 			children = (
+				CC76E9482BD471BA008BEB61 /* Forecast+CoreDataClass.swift */,
+				CC76E9492BD471BA008BEB61 /* Forecast+CoreDataProperties.swift */,
+				CC76E94A2BD471BA008BEB61 /* ForecastValue+CoreDataClass.swift */,
+				CC76E94B2BD471BA008BEB61 /* ForecastValue+CoreDataProperties.swift */,
 				5856174B2BDADA3F009B23D7 /* GlucoseStored+CoreDataClass.swift */,
 				5856174C2BDADA3F009B23D7 /* GlucoseStored+CoreDataProperties.swift */,
 				588752822BD9986A008B081D /* OpenAPS_Battery+CoreDataClass.swift */,
@@ -2199,6 +2213,7 @@
 				58F107732BD1A4D000B1A680 /* Determination+helper.swift */,
 				5837A52F2BD2E3C700A5DC04 /* MealsStored+helper.swift */,
 				5837A5312BD2E81100A5DC04 /* InsulinStored+helper.swift */,
+				CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */,
 				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
 			);
 			path = Helper;
@@ -2955,6 +2970,10 @@
 				5825D1452BD4058F00F36E9B /* Autosens_+CoreDataProperties.swift in Sources */,
 				19DC678529CA67A400FD9EC4 /* OverrideProfilesRootView.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* MealsStored+helper.swift in Sources */,
+				CC76E94C2BD471BA008BEB61 /* Forecast+CoreDataClass.swift in Sources */,
+				CC76E94D2BD471BA008BEB61 /* Forecast+CoreDataProperties.swift in Sources */,
+				CC76E94E2BD471BA008BEB61 /* ForecastValue+CoreDataClass.swift in Sources */,
+				CC76E94F2BD471BA008BEB61 /* ForecastValue+CoreDataProperties.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
@@ -3100,6 +3119,7 @@
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				5825D13C2BD4058F00F36E9B /* ImportError+CoreDataClass.swift in Sources */,
 				5825D13F2BD4058F00F36E9B /* TDD+CoreDataProperties.swift in Sources */,
+				CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */,
 				F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */,
 				19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */,
 				5825D1432BD4058F00F36E9B /* Presets+CoreDataProperties.swift in Sources */,

+ 86 - 38
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -82,45 +82,63 @@ final class OpenAPS {
                 if var determination = Determination(from: orefDetermination) {
                     determination.timestamp = determination.deliverAt ?? clock
                     self.storage.save(determination, as: Enact.suggested)
-
-                    // save to core data asynchronously and on a private queue
+                    // save to core data asynchronously
                     self.context.perform {
-                        let new = OrefDetermination(context: self.context)
-                        new.id = UUID()
-                        new.totalDailyDose = determination.tdd as? NSDecimalNumber
-                        new.insulinSensitivity = determination.isf as? NSDecimalNumber
-                        new.currentTarget = determination.current_target as? NSDecimalNumber
-                        new.eventualBG = determination.eventualBG as? NSDecimalNumber
-                        new.deliverAt = determination.deliverAt
-                        new.enacted = false
-                        new.insulinForManualBolus = determination.insulinForManualBolus as? NSDecimalNumber
-                        new.carbRatio = determination.carbRatio as? NSDecimalNumber
-                        new.glucose = determination.bg as? NSDecimalNumber
-                        new.reservoir = determination.reservoir as? NSDecimalNumber
-                        new.insulinReq = determination.insulinReq as? NSDecimalNumber
-                        new.temp = determination.temp?.rawValue ?? "absolute"
-                        new.rate = determination.rate as? NSDecimalNumber
-                        new.reason = determination.reason
-                        new.duration = Int16(determination.duration ?? 0)
-                        new.iob = determination.iob as? NSDecimalNumber
-                        new.treshold = determination.treshold as? NSDecimalNumber
-//                                                new.predBGs: [
-//                                                "zt": immutableDetermination.predictions?.zt ?? [],
-//                                                "cob": immutableDetermination.predictions?.cob ?? [],
-//                                                "iob": immutableDetermination.predictions?.iob ?? [],
-//                                                "uam": immutableDetermination.predictions?.uam ?? []
-//                                            ]
-                        new.minDelta = determination.minDelta as? NSDecimalNumber
-                        new.sensitivityRatio = determination.sensitivityRatio as? NSDecimalNumber
-                        new.expectedDelta = determination.expectedDelta as? NSDecimalNumber
-                        new.cob = Int16(Int(determination.cob ?? 0))
-                        new.manualBolusErrorString = determination.manualBolusErrorString as? NSDecimalNumber
-                        new.timestampEnacted = nil
-                        new.tempBasal = determination.insulin?.temp_basal as? NSDecimalNumber
-                        new.scheduledBasal = determination.insulin?.scheduled_basal as? NSDecimalNumber
-                        new.bolus = determination.insulin?.bolus as? NSDecimalNumber
-                        new.smbToDeliver = determination.units as? NSDecimalNumber
-                        new.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
+                        let newOrefDetermination = OrefDetermination(context: self.context)
+                        newOrefDetermination.id = UUID()
+                        newOrefDetermination.totalDailyDose = determination.tdd as? NSDecimalNumber
+                        newOrefDetermination.insulinSensitivity = determination.isf as? NSDecimalNumber
+                        newOrefDetermination.currentTarget = determination.current_target as? NSDecimalNumber
+                        newOrefDetermination.eventualBG = determination.eventualBG as? NSDecimalNumber
+                        newOrefDetermination.deliverAt = determination.deliverAt
+                        newOrefDetermination.enacted = false
+                        newOrefDetermination.insulinForManualBolus = determination.insulinForManualBolus as? NSDecimalNumber
+                        newOrefDetermination.carbRatio = determination.carbRatio as? NSDecimalNumber
+                        newOrefDetermination.glucose = determination.bg as? NSDecimalNumber
+                        newOrefDetermination.reservoir = determination.reservoir as? NSDecimalNumber
+                        newOrefDetermination.insulinReq = determination.insulinReq as? NSDecimalNumber
+                        newOrefDetermination.temp = determination.temp?.rawValue ?? "absolute"
+                        newOrefDetermination.rate = determination.rate as? NSDecimalNumber
+                        newOrefDetermination.reason = determination.reason
+                        newOrefDetermination.duration = Int16(determination.duration ?? 0)
+                        newOrefDetermination.iob = determination.iob as? NSDecimalNumber
+                        newOrefDetermination.treshold = determination.treshold as? NSDecimalNumber
+
+                        if let predictions = determination.predictions {
+                            // Create forecasts for each type
+                            ["iob": predictions.iob, "zt": predictions.zt, "cob": predictions.cob, "uam": predictions.uam]
+                                .forEach { type, values in
+                                    if let values = values {
+                                        let forecast = Forecast(context: self.context)
+                                        forecast.id = UUID()
+                                        forecast.type = type
+                                        forecast.date = Date() // or use a specific timestamp if available
+                                        forecast
+                                            .orefDetermination =
+                                            newOrefDetermination // Assuming a relationship from Forecast to OrefDetermination
+
+                                        for (index, value) in values.enumerated() {
+                                            let forecastValue = ForecastValue(context: self.context)
+                                            forecastValue.index = Int32(index)
+                                            forecastValue.value = Int32(value)
+                                            forecast.addToForecastValues(forecastValue)
+                                        }
+                                        newOrefDetermination.addToForecasts(forecast)
+                                    }
+                                }
+                        }
+
+                        newOrefDetermination.minDelta = determination.minDelta as? NSDecimalNumber
+                        newOrefDetermination.sensitivityRatio = determination.sensitivityRatio as? NSDecimalNumber
+                        newOrefDetermination.expectedDelta = determination.expectedDelta as? NSDecimalNumber
+                        newOrefDetermination.cob = Int16(Int(determination.cob ?? 0))
+                        newOrefDetermination.manualBolusErrorString = determination.manualBolusErrorString as? NSDecimalNumber
+                        newOrefDetermination.timestampEnacted = nil
+                        newOrefDetermination.tempBasal = determination.insulin?.temp_basal as? NSDecimalNumber
+                        newOrefDetermination.scheduledBasal = determination.insulin?.scheduled_basal as? NSDecimalNumber
+                        newOrefDetermination.bolus = determination.insulin?.bolus as? NSDecimalNumber
+                        newOrefDetermination.smbToDeliver = determination.units as? NSDecimalNumber
+                        newOrefDetermination.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
                         do {
                             try self.context.save()
                             debugPrint(
@@ -702,4 +720,34 @@ final class OpenAPS {
         }
         return (try? String(contentsOf: url)) ?? ""
     }
+
+    func processAndSave(forecastData: [String: [Int]]) {
+        let context = self.context
+
+        let currentDate = Date()
+
+        for (type, values) in forecastData {
+            createForecast(type: type, values: values, date: currentDate, context: context)
+        }
+    }
+
+    func createForecast(type: String, values: [Int], date: Date, context: NSManagedObjectContext) {
+        let forecast = Forecast(context: context)
+        forecast.id = UUID()
+        forecast.date = date
+        forecast.type = type
+
+        for (index, value) in values.enumerated() {
+            let forecastValue = ForecastValue(context: context)
+            forecastValue.value = Int32(value)
+            forecastValue.index = Int32(index)
+            forecastValue.forecast = forecast
+        }
+
+        do {
+            try context.save()
+        } catch {
+            print("Failed to save forecast: \(error)")
+        }
+    }
 }

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

@@ -117,6 +117,11 @@ struct MainChartView: View {
         animation: Animation.bouncy
     ) var determinations: FetchedResults<OrefDetermination>
 
+    @FetchRequest(
+        fetchRequest: Forecast.fetch(NSPredicate.predicateFor30MinAgo, ascending: false),
+        animation: .default
+    ) var forecasts: FetchedResults<Forecast>
+
     private var bolusFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
@@ -166,7 +171,6 @@ struct MainChartView: View {
                     LazyVStack(spacing: 0) {
                         mainChart
                         basalChart
-
                     }.onChange(of: screenHours) { _ in
                         updateStartEndMarkers()
                         yAxisChartData()
@@ -229,7 +233,7 @@ extension MainChartView {
                 drawFpus()
                 drawBoluses()
                 drawTempTargets()
-                drawPredictions()
+                drawForecasts()
                 drawGlucose()
                 drawManualGlucose()
 
@@ -253,9 +257,6 @@ extension MainChartView {
                 }
             }
             .id("MainChart")
-            .onChange(of: glucoseFromPersistence.map(\.id)) { _ in
-                calculatePredictions()
-            }
             .onChange(of: boluses) { _ in
                 state.roundedTotalBolus = state.calculateTINS()
             }
@@ -263,26 +264,22 @@ extension MainChartView {
                 calculateTTs()
             }
             .onChange(of: didAppearTrigger) { _ in
-                calculatePredictions()
                 calculateTTs()
             }
-            .onChange(of: determinations.map(\.id)) { _ in
-                calculatePredictions()
-            }
-            .onReceive(
-                Foundation.NotificationCenter.default
-                    .publisher(for: UIApplication.willEnterForegroundNotification)
-            ) { _ in
-                calculatePredictions()
-            }
             .frame(minHeight: UIScreen.main.bounds.height * 0.3)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)
             .chartXAxis { mainChartXAxis }
-            // .chartXAxis(.hidden)
+            .backport.chartXSelection(value: $selection)
             .chartYAxis { mainChartYAxis }
             .chartYScale(domain: minValue ... maxValue)
-            .backport.chartXSelection(value: $selection)
+            .chartForegroundStyleScale([
+                "zt": Color.zt,
+                "uam": Color.uam,
+                "cob": .orange,
+                "iob": .blue
+            ])
+            .chartLegend(.hidden)
         }
     }
 
@@ -466,39 +463,46 @@ extension MainChartView {
         }
     }
 
-    private func drawPredictions() -> some ChartContent {
-        /// predictions
-        ForEach(Predictions, id: \.self) { info in
-            let y = max(info.amount, 0)
+    private func timeForIndex(_ index: Int32) -> Date {
+        let currentTime = Date()
+        let timeInterval = TimeInterval(index * 300)
+        return currentTime.addingTimeInterval(timeInterval)
+    }
 
-            if info.type == .uam {
-                LineMark(
-                    x: .value("Time", info.timestamp, unit: .second),
-                    y: .value("Value", Decimal(y) * conversionFactor),
-                    series: .value("uam", "uam")
-                ).foregroundStyle(Color.uam).symbolSize(16)
-            }
-            if info.type == .cob {
-                LineMark(
-                    x: .value("Time", info.timestamp, unit: .second),
-                    y: .value("Value", Decimal(y) * conversionFactor),
-                    series: .value("cob", "cob")
-                ).foregroundStyle(Color.orange).symbolSize(16)
-            }
-            if info.type == .iob {
-                LineMark(
-                    x: .value("Time", info.timestamp, unit: .second),
-                    y: .value("Value", Decimal(y) * conversionFactor),
-                    series: .value("iob", "iob")
-                ).foregroundStyle(Color.insulin).symbolSize(16)
-            }
-            if info.type == .zt {
-                LineMark(
-                    x: .value("Time", info.timestamp, unit: .second),
-                    y: .value("Value", Decimal(y) * conversionFactor),
-                    series: .value("zt", "zt")
-                ).foregroundStyle(Color.zt).symbolSize(16)
+    private func getForecasts(_ determination: OrefDetermination) -> [Forecast] {
+        guard let forecastSet = determination.forecasts, let forecasts = Array(forecastSet) as? [Forecast] else {
+            return []
+        }
+
+        return forecasts
+    }
+
+    private func getForecastValues(_ forecast: Forecast) -> [ForecastValue] {
+        guard let forecastValueSet = forecast.forecastValues,
+              let forecastValues = Array(forecastValueSet) as? [ForecastValue]
+        else {
+            return []
+        }
+
+        return forecastValues.sorted(by: { $0.index < $1.index })
+    }
+
+    private func drawForecasts() -> some ChartContent {
+        /// for every determination in determinations get the forecasts
+        ForEach(determinations.flatMap { determination -> [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] in
+            let forecasts = getForecasts(determination) /// returns array of Forecast objects
+            /// now get the values for every forecast and add it to a tuple, identify it with an ID
+            return forecasts.flatMap { forecast in
+                getForecastValues(forecast).map { forecastValue in
+                    (id: UUID(), forecast: forecast, forecastValue: forecastValue)
+                }
             }
+        }, id: \.id) { tuple in
+            LineMark(
+                x: .value("Time", timeForIndex(tuple.forecastValue.index)),
+                y: .value("Value", Int(tuple.forecastValue.value))
+            )
+            .foregroundStyle(by: .value("Predictions", tuple.forecast.type ?? ""))
         }
     }
 
@@ -588,52 +592,36 @@ extension MainChartView {
         }
     }
 
-    private func drawTempBasals() -> some ChartContent {
-        /// temp basal rects
-        ForEach(TempBasals) { temp in
-            /// calculate end time of temp basal adding duration to start time
-            let end = temp.timestamp + (temp.durationMin ?? 0).minutes.timeInterval
-            let now = Date()
-
-            /// ensure that temp basals that are set cannot exceed current date -> i.e. scheduled temp basals are not shown
-            /// we could display scheduled temp basals with opacity etc... in the future
-            let maxEndTime = min(end, now)
+    private func filteredTempBasals() -> [(start: Date, end: Date, rate: Double)] {
+        let now = Date()
+        return TempBasals.compactMap { temp -> (start: Date, end: Date, rate: Double)? in
+            let end = min(temp.timestamp + (temp.durationMin ?? 0).minutes.timeInterval, now)
+            let isInsulinSuspended = suspensions.contains { $0.timestamp >= temp.timestamp && $0.timestamp <= end }
 
-            /// set mark height to 0 when insulin delivery is suspended
-            let isInsulinSuspended = suspensions
-                .first(where: { $0.timestamp >= temp.timestamp && $0.timestamp <= maxEndTime }) != nil
-            let rate = (temp.rate ?? 0) * (isInsulinSuspended ? 0 : 1)
+            let rate = Double(temp.rate ?? Decimal.zero) * (isInsulinSuspended ? 0 : 1)
 
-            /// find next basal entry and if available set end of current entry to start of next entry
-            if let nextTemp = TempBasals.first(where: { $0.timestamp > temp.timestamp }) {
-                let nextTempStart = nextTemp.timestamp
-
-                RectangleMark(
-                    xStart: .value("start", temp.timestamp),
-                    xEnd: .value("end", nextTempStart),
-                    yStart: .value("rate-start", 0),
-                    yEnd: .value("rate-end", rate)
-                ).foregroundStyle(Color.insulin.opacity(0.2))
-
-                LineMark(x: .value("Start Date", temp.timestamp), y: .value("Amount", rate))
-                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+            // Check if there's a subsequent temp basal to determine the end time
+            guard let nextTemp = TempBasals.first(where: { $0.timestamp > temp.timestamp }) else {
+                return (temp.timestamp, end, rate)
+            }
+            return (temp.timestamp, nextTemp.timestamp, rate)
+        }
+    }
 
-                LineMark(x: .value("End Date", nextTempStart), y: .value("Amount", rate))
-                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
-            } else {
-                RectangleMark(
-                    xStart: .value("start", temp.timestamp),
-                    xEnd: .value("end", maxEndTime),
-                    yStart: .value("rate-start", 0),
-                    yEnd: .value("rate-end", rate)
-                ).foregroundStyle(Color.insulin.opacity(0.2))
+    private func drawTempBasals() -> some ChartContent {
+        ForEach(filteredTempBasals(), id: \.start) { basal in
+            RectangleMark(
+                xStart: .value("start", basal.start),
+                xEnd: .value("end", basal.end),
+                yStart: .value("rate-start", 0),
+                yEnd: .value("rate-end", basal.rate)
+            ).foregroundStyle(Color.insulin.opacity(0.2))
 
-                LineMark(x: .value("Start Date", temp.timestamp), y: .value("Amount", rate))
-                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+            LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
+                .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
 
-                LineMark(x: .value("End Date", maxEndTime), y: .value("Amount", rate))
-                    .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
-            }
+            LineMark(x: .value("End Date", basal.end), y: .value("Amount", basal.rate))
+                .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
         }
     }
 
@@ -740,36 +728,6 @@ extension MainChartView {
         ChartTempTargets = calculatedTTs
     }
 
-    private func addPredictions(_ predictions: [Int], type: PredictionType, deliveredAt: Date, endMarker: Date) -> [Prediction] {
-        var calculatedPredictions: [Prediction] = []
-        predictions.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes.timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: predictions[index], timestamp: predTime, type: type)
-                )
-            }
-        }
-        return calculatedPredictions
-    }
-
-    private func calculatePredictions() {
-//        guard let suggestion = suggestion, let deliveredAt = suggestion.deliverAt else { return }
-//        let uamPredictions = suggestion.predictions?.uam ?? []
-//        let iobPredictions = suggestion.predictions?.iob ?? []
-//        let cobPredictions = suggestion.predictions?.cob ?? []
-//        let ztPredictions = suggestion.predictions?.zt ?? []
-//
-//        let uam = addPredictions(uamPredictions, type: .uam, deliveredAt: deliveredAt, endMarker: endMarker)
-//        let iob = addPredictions(iobPredictions, type: .iob, deliveredAt: deliveredAt, endMarker: endMarker)
-//        let cob = addPredictions(cobPredictions, type: .cob, deliveredAt: deliveredAt, endMarker: endMarker)
-//        let zt = addPredictions(ztPredictions, type: .zt, deliveredAt: deliveredAt, endMarker: endMarker)
-//
-//        Predictions = uam + iob + cob + zt
-    }
-
     private func calculateTempBasals() {
         let basals = tempBasals
         var returnTempBasalRates: [PumpHistoryEvent] = []

+ 14 - 1
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="22222" systemVersion="22G120" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E224" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="Autosens_" representedClassName="Autosens_" syncable="YES">
         <attribute name="newisf" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="ratio" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -25,6 +25,18 @@
         <attribute name="median_7" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="median_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
     </entity>
+    <entity name="Forecast" representedClassName="Forecast" syncable="YES">
+        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="type" optional="YES" attributeType="String"/>
+        <relationship name="forecastValues" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ForecastValue" inverseName="forecast" inverseEntity="ForecastValue"/>
+        <relationship name="orefDetermination" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="OrefDetermination" inverseName="forecasts" inverseEntity="OrefDetermination"/>
+    </entity>
+    <entity name="ForecastValue" representedClassName="ForecastValue" syncable="YES">
+        <attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="value" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
+        <relationship name="forecast" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Forecast" inverseName="forecastValues" inverseEntity="Forecast"/>
+    </entity>
     <entity name="GlucoseStored" representedClassName="GlucoseStored" syncable="YES">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="direction" optional="YES" attributeType="String"/>
@@ -122,6 +134,7 @@
         <attribute name="timestampEnacted" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="totalDailyDose" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="treshold" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <relationship name="forecasts" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Forecast" inverseName="orefDetermination" inverseEntity="Forecast"/>
     </entity>
     <entity name="Override" representedClassName="Override" syncable="YES">
         <attribute name="advancedSettings" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>

+ 1 - 0
Model/Helper/Determination+helper.swift

@@ -7,6 +7,7 @@ extension OrefDetermination {
         request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
         request.predicate = predicate
         request.fetchLimit = 1
+        request.returnsObjectsAsFaults = true
         return request
     }
 }

+ 19 - 0
Model/Helper/Forecast+helper.swift

@@ -0,0 +1,19 @@
+import CoreData
+import Foundation
+
+public extension Forecast {
+    static func fetch(_ predicate: NSPredicate, ascending: Bool) -> NSFetchRequest<Forecast> {
+        let request = NSFetchRequest<Forecast>(entityName: "Forecast")
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \Forecast.date, ascending: ascending)]
+        request.fetchLimit = 1
+        request.predicate = predicate
+        request.returnsObjectsAsFaults = true
+
+        return request
+    }
+
+    var forecastValuesArray: [ForecastValue] {
+        let set = forecastValues ?? []
+        return set.sorted { $0.index < $1.index }
+    }
+}

+ 17 - 0
OrefDetermination+CoreDataProperties.swift

@@ -37,6 +37,23 @@ public extension OrefDetermination {
     @NSManaged var timestampEnacted: Date?
     @NSManaged var totalDailyDose: NSDecimalNumber?
     @NSManaged var treshold: NSDecimalNumber?
+    @NSManaged var forecasts: Set<Forecast>?
+}
+
+// MARK: Generated accessors for forecasts
+
+public extension OrefDetermination {
+    @objc(addForecastsObject:)
+    @NSManaged func addToForecasts(_ value: Forecast)
+
+    @objc(removeForecastsObject:)
+    @NSManaged func removeFromForecasts(_ value: Forecast)
+
+    @objc(addForecasts:)
+    @NSManaged func addToForecasts(_ values: NSSet)
+
+    @objc(removeForecasts:)
+    @NSManaged func removeFromForecasts(_ values: NSSet)
 }
 
 extension OrefDetermination: Identifiable {}