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

Merge branch 'core-data' of github.com:polscm32/Open-iAPS into core-data

dnzxy 2 лет назад
Родитель
Сommit
c98bd0cc47
27 измененных файлов с 407 добавлено и 270 удалено
  1. 20 8
      FreeAPS.xcodeproj/project.pbxproj
  2. 34 10
      FreeAPS/Sources/APS/APSManager.swift
  3. 20 3
      FreeAPS/Sources/APS/DeviceDataManager.swift
  4. 0 1
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  5. 19 20
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  6. 1 1
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  7. 12 1
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  8. 7 2
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  9. 20 1
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  10. 0 2
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  11. 0 10
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  12. 1 89
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  13. 29 33
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  14. 12 10
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  15. 11 5
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  16. 7 7
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  17. 1 0
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift
  18. 5 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  19. 34 1
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  20. 1 0
      GlucoseStored+CoreDataProperties.swift
  21. 103 61
      LiveActivity/LiveActivity.swift
  22. 9 0
      Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  23. 25 1
      Model/Helper/GlucoseStored+helper.swift
  24. 3 2
      Model/Helper/InsulinStored+helper.swift
  25. 12 0
      Model/Helper/OpenAPSBattery.swift
  26. 4 0
      OpenAPS_Battery+CoreDataClass.swift
  27. 17 0
      OpenAPS_Battery+CoreDataProperties.swift

+ 20 - 8
FreeAPS.xcodeproj/project.pbxproj

@@ -311,13 +311,16 @@
 		5825D15D2BD4058F00F36E9B /* Target+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D12F2BD4058F00F36E9B /* Target+CoreDataProperties.swift */; };
 		5825D15E2BD4058F00F36E9B /* Protein+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1302BD4058F00F36E9B /* Protein+CoreDataClass.swift */; };
 		5825D15F2BD4058F00F36E9B /* Protein+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1312BD4058F00F36E9B /* Protein+CoreDataProperties.swift */; };
-		5825D1602BD4058F00F36E9B /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1322BD4058F00F36E9B /* GlucoseStored+CoreDataClass.swift */; };
-		5825D1612BD4058F00F36E9B /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1332BD4058F00F36E9B /* GlucoseStored+CoreDataProperties.swift */; };
 		583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684052BD178DB00070A60 /* GlucoseStored+helper.swift */; };
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
 		5837A5302BD2E3C700A5DC04 /* MealsStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5837A52F2BD2E3C700A5DC04 /* MealsStored+helper.swift */; };
 		5837A5322BD2E81100A5DC04 /* InsulinStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5837A5312BD2E81100A5DC04 /* InsulinStored+helper.swift */; };
+		5856174D2BDADA3F009B23D7 /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5856174B2BDADA3F009B23D7 /* GlucoseStored+CoreDataClass.swift */; };
+		5856174E2BDADA3F009B23D7 /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5856174C2BDADA3F009B23D7 /* GlucoseStored+CoreDataProperties.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
+		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
+		588752842BD9986A008B081D /* OpenAPS_Battery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588752822BD9986A008B081D /* OpenAPS_Battery+CoreDataClass.swift */; };
+		588752852BD9986A008B081D /* OpenAPS_Battery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588752832BD9986A008B081D /* OpenAPS_Battery+CoreDataProperties.swift */; };
 		58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F107732BD1A4D000B1A680 /* Determination+helper.swift */; };
 		5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */; };
@@ -928,13 +931,16 @@
 		5825D12F2BD4058F00F36E9B /* Target+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Target+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		5825D1302BD4058F00F36E9B /* Protein+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Protein+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		5825D1312BD4058F00F36E9B /* Protein+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Protein+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
-		5825D1322BD4058F00F36E9B /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
-		5825D1332BD4058F00F36E9B /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		583684052BD178DB00070A60 /* GlucoseStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+helper.swift"; sourceTree = "<group>"; };
 		583684072BD195A700070A60 /* Determination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Determination.swift; sourceTree = "<group>"; };
 		5837A52F2BD2E3C700A5DC04 /* MealsStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealsStored+helper.swift"; sourceTree = "<group>"; };
 		5837A5312BD2E81100A5DC04 /* InsulinStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InsulinStored+helper.swift"; sourceTree = "<group>"; };
+		5856174B2BDADA3F009B23D7 /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		5856174C2BDADA3F009B23D7 /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowView.swift; sourceTree = "<group>"; };
+		5887527B2BD986E1008B081D /* OpenAPSBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSBattery.swift; sourceTree = "<group>"; };
+		588752822BD9986A008B081D /* OpenAPS_Battery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		588752832BD9986A008B081D /* OpenAPS_Battery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		58F107732BD1A4D000B1A680 /* Determination+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Determination+helper.swift"; sourceTree = "<group>"; };
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
@@ -2146,6 +2152,10 @@
 				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 */,
+				588752832BD9986A008B081D /* OpenAPS_Battery+CoreDataProperties.swift */,
 				5825D1062BD4058F00F36E9B /* BGaverages+CoreDataClass.swift */,
 				5825D1072BD4058F00F36E9B /* BGaverages+CoreDataProperties.swift */,
 				5825D1082BD4058F00F36E9B /* BGmedian+CoreDataClass.swift */,
@@ -2190,8 +2200,6 @@
 				5825D12F2BD4058F00F36E9B /* Target+CoreDataProperties.swift */,
 				5825D1302BD4058F00F36E9B /* Protein+CoreDataClass.swift */,
 				5825D1312BD4058F00F36E9B /* Protein+CoreDataProperties.swift */,
-				5825D1322BD4058F00F36E9B /* GlucoseStored+CoreDataClass.swift */,
-				5825D1332BD4058F00F36E9B /* GlucoseStored+CoreDataProperties.swift */,
 			);
 			path = "Classes+Properties";
 			sourceTree = "<group>";
@@ -2206,6 +2214,7 @@
 				5837A52F2BD2E3C700A5DC04 /* MealsStored+helper.swift */,
 				5837A5312BD2E81100A5DC04 /* InsulinStored+helper.swift */,
 				CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */,
+				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -2941,8 +2950,10 @@
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				5825D1522BD4058F00F36E9B /* LastLoop+CoreDataClass.swift in Sources */,
+				588752852BD9986A008B081D /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				5825D13A2BD4058F00F36E9B /* LoopStatRecord+CoreDataClass.swift in Sources */,
+				5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
 				5825D15B2BD4058F00F36E9B /* OverridePresets+CoreDataProperties.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
@@ -3072,7 +3083,6 @@
 				19DC677F29CA675700FD9EC4 /* OverrideProfilesDataFlow.swift in Sources */,
 				1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */,
 				CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */,
-				5825D1612BD4058F00F36E9B /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
@@ -3138,6 +3148,7 @@
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
+				5856174E2BDADA3F009B23D7 /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
@@ -3174,7 +3185,6 @@
 				CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */,
 				38E8755427561E9800975559 /* DataFlow.swift in Sources */,
 				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
-				5825D1602BD4058F00F36E9B /* GlucoseStored+CoreDataClass.swift in Sources */,
 				CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */,
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
@@ -3184,6 +3194,7 @@
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
+				5856174D2BDADA3F009B23D7 /* GlucoseStored+CoreDataClass.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
@@ -3199,6 +3210,7 @@
 				19E1F7EC29D082FE005C8D20 /* IconConfigStateModel.swift in Sources */,
 				5837A5322BD2E81100A5DC04 /* InsulinStored+helper.swift in Sources */,
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
+				588752842BD9986A008B081D /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */,
 				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,

+ 34 - 10
FreeAPS/Sources/APS/APSManager.swift

@@ -330,22 +330,23 @@ final class BaseAPSManager: APSManager, Injectable {
 
     func determineBasal() -> AnyPublisher<Bool, Never> {
         debug(.apsManager, "Start determine basal")
-        guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.isNotEmpty else {
+        let glucose = fetchGlucoseData(forPeriod: "30min", withPredicate: NSPredicate.predicateFor30MinAgo, fetchLimit: 4)
+        guard glucose.count > 2 else {
             debug(.apsManager, "Not enough glucose data")
             processError(APSError.glucoseError(message: "Not enough glucose data"))
             return Just(false).eraseToAnyPublisher()
         }
 
-        let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
-        guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
+        let dateOfLastGlucose = glucose.first?.date
+        guard dateOfLastGlucose ?? Date() >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
             debug(.apsManager, "Glucose data is stale")
             processError(APSError.glucoseError(message: "Glucose data is stale"))
             return Just(false).eraseToAnyPublisher()
         }
 
         // Only let glucose be flat when 400 mg/dl
-        if (glucoseStorage.recent().last?.glucose ?? 100) != 400 {
-            guard glucoseStorage.isGlucoseNotFlat() else {
+        if (glucose.first?.glucose ?? 100) != 400 {
+            guard !GlucoseStored.glucoseIsFlat(glucose) else {
                 debug(.apsManager, "Glucose data is too flat")
                 processError(APSError.glucoseError(message: "Glucose data is too flat"))
                 return Just(false).eraseToAnyPublisher()
@@ -675,13 +676,13 @@ final class BaseAPSManager: APSManager, Injectable {
                 return Fail(error: error).eraseToAnyPublisher()
             }
 
-            if determination.rate == 0 || determination.duration == 0 {
+            guard let rate = determination.rate else {
                 debug(.apsManager, "No temp required")
                 return Just(()).setFailureType(to: Error.self)
                     .eraseToAnyPublisher()
             }
             return pump.enactTempBasal(
-                unitsPerHour: Double(truncating: determination.rate ?? 0),
+                unitsPerHour: Double(truncating: rate),
                 for: TimeInterval(determination.duration * 60)
             ).map { _ in
                 let temp = TempBasal(
@@ -905,9 +906,13 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     // fetch glucose for time interval
-    private func fetchGlucoseData(forPeriod period: String, withPredicate predicate: NSPredicate) -> [GlucoseStored] {
+    private func fetchGlucoseData(
+        forPeriod period: String,
+        withPredicate predicate: NSPredicate,
+        fetchLimit: Int? = nil
+    ) -> [GlucoseStored] {
         do {
-            let fetchedData = try viewContext.fetch(GlucoseStored.fetch(predicate, ascending: false))
+            let fetchedData = try privateContext.fetch(GlucoseStored.fetch(predicate, ascending: false, fetchLimit: fetchLimit))
             debugPrint(
                 "APSManager: \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) fetched glucose for \(period) period"
             )
@@ -1367,7 +1372,26 @@ extension BaseAPSManager: PumpManagerStatusObserver {
             string: percent > 10 ? .normal : .low,
             display: status.pumpBatteryChargeRemaining != nil
         )
-        storage.save(battery, as: OpenAPS.Monitor.battery)
+
+        let batteryToStore = OpenAPS_Battery(context: privateContext)
+        batteryToStore.id = UUID()
+        batteryToStore.date = Date()
+        batteryToStore.percent = Int16(percent)
+        batteryToStore.voltage = nil
+        batteryToStore.status = percent > 10 ? "normal" : "low"
+        batteryToStore.display = status.pumpBatteryChargeRemaining != nil
+        privateContext.perform {
+            do {
+                try self.privateContext.save()
+                debugPrint(
+                    "APS Manager: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) saved battery infos to core data"
+                )
+            } catch {
+                debugPrint(
+                    "APS Manager: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to save battery infos to core data"
+                )
+            }
+        }
         storage.save(status.pumpStatus, as: OpenAPS.Monitor.status)
     }
 }

+ 20 - 3
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -73,6 +73,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
     private var pumpUpdatePromise: Future<Bool, Never>.Promise?
     @SyncAccess var loopInProgress: Bool = false
+    private let privateContext = CoreDataStack.shared.backgroundContext
 
     var pumpManager: PumpManagerUI? {
         didSet {
@@ -345,9 +346,25 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             string: batteryPercent >= 10 ? .normal : .low,
             display: pumpManager.status.pumpBatteryChargeRemaining != nil
         )
-        storage.save(battery, as: OpenAPS.Monitor.battery)
-        broadcaster.notify(PumpBatteryObserver.self, on: processQueue) {
-            $0.pumpBatteryDidChange(battery)
+
+        let batteryToStore = OpenAPS_Battery(context: 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
+        privateContext.perform {
+            do {
+                try self.privateContext.save()
+                debugPrint(
+                    "Device Data manager: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) saved battery infos to core data"
+                )
+            } catch {
+                debugPrint(
+                    "Device Data manager: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to save battery infos to core data"
+                )
+            }
         }
         broadcaster.notify(PumpTimeZoneObserver.self, on: processQueue) {
             $0.pumpTimeZoneDidChange(status.timeZone)

+ 0 - 1
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -44,7 +44,6 @@ extension OpenAPS {
     enum Monitor {
         static let pumpHistory = "monitor/pumphistory-24h-zoned.json"
         static let reservoir = "monitor/reservoir.json"
-        static let battery = "monitor/battery.json"
         static let carbHistory = "monitor/carbhistory.json"
         static let clock = "monitor/clock-zoned.json"
         static let status = "monitor/status.json"

+ 19 - 20
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -9,7 +9,7 @@ final class OpenAPS {
 
     private let storage: FileStorage
 
-    let viewContext = CoreDataStack.shared.viewContext
+    let context = CoreDataStack.shared.backgroundContext
 
     init(storage: FileStorage) {
         self.storage = storage
@@ -82,10 +82,9 @@ 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
-                    self.viewContext.perform {
-                        let newOrefDetermination = OrefDetermination(context: self.viewContext)
+                    self.context.perform {
+                        let newOrefDetermination = OrefDetermination(context: self.context)
                         newOrefDetermination.id = UUID()
                         newOrefDetermination.totalDailyDose = determination.tdd as? NSDecimalNumber
                         newOrefDetermination.insulinSensitivity = determination.isf as? NSDecimalNumber
@@ -110,7 +109,7 @@ final class OpenAPS {
                             ["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.viewContext)
+                                        let forecast = Forecast(context: self.context)
                                         forecast.id = UUID()
                                         forecast.type = type
                                         forecast.date = Date() // or use a specific timestamp if available
@@ -119,7 +118,7 @@ final class OpenAPS {
                                             newOrefDetermination // Assuming a relationship from Forecast to OrefDetermination
 
                                         for (index, value) in values.enumerated() {
-                                            let forecastValue = ForecastValue(context: self.viewContext)
+                                            let forecastValue = ForecastValue(context: self.context)
                                             forecastValue.index = Int32(index)
                                             forecastValue.value = Int32(value)
                                             forecast.addToForecastValues(forecastValue)
@@ -141,7 +140,7 @@ final class OpenAPS {
                         newOrefDetermination.smbToDeliver = determination.units as? NSDecimalNumber
                         newOrefDetermination.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
                         do {
-                            try self.viewContext.save()
+                            try self.context.save()
                             debugPrint(
                                 "OpenAPS: \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) saved determination"
                             )
@@ -155,16 +154,16 @@ final class OpenAPS {
                     // MARK: Save to CoreData also. To do: Remove JSON saving
 
                     if determination.tdd ?? 0 > 0 {
-                        self.viewContext.perform {
-                            let saveToTDD = TDD(context: self.viewContext)
+                        self.context.perform {
+                            let saveToTDD = TDD(context: self.context)
 
                             saveToTDD.timestamp = determination.timestamp ?? Date()
                             saveToTDD.tdd = (determination.tdd ?? 0) as NSDecimalNumber?
-                            try? self.viewContext.save()
+                            try? self.context.save()
 
-                            let saveTarget = Target(context: self.viewContext)
+                            let saveTarget = Target(context: self.context)
                             saveTarget.current = (determination.current_target ?? 100) as NSDecimalNumber?
-                            try? self.viewContext.save()
+                            try? self.context.save()
                         }
 
 //                        self.coredataContext.perform {
@@ -188,7 +187,7 @@ final class OpenAPS {
     }
 
     func oref2() -> RawJSON {
-        viewContext.performAndWait {
+        context.performAndWait {
             let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             var hbt_ = preferences?.halfBasalExerciseTarget ?? 160
             let wp = preferences?.weightPercentage ?? 1
@@ -203,28 +202,28 @@ final class OpenAPS {
             requestTDD.predicate = NSPredicate(format: "timestamp > %@ AND tdd > 0", tenDaysAgo as NSDate)
             let sortTDD = NSSortDescriptor(key: "timestamp", ascending: true)
             requestTDD.sortDescriptors = [sortTDD]
-            try? uniqueEvents = viewContext.fetch(requestTDD)
+            try? uniqueEvents = 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 = viewContext.fetch(requestIsEnbled)
+            try? sliderArray = 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 = viewContext.fetch(requestOverrides)
+            try? overrideArray = 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 = viewContext.fetch(requestTempTargets)
+            try? tempTargetsArray = context.fetch(requestTempTargets)
 
             let total = uniqueEvents.compactMap({ each in each.tdd as? Decimal ?? 0 }).reduce(0, +)
             var indeces = uniqueEvents.count
@@ -270,13 +269,13 @@ final class OpenAPS {
                    !unlimited
                 {
                     useOverride = false
-                    let saveToCoreData = Override(context: self.viewContext)
+                    let saveToCoreData = Override(context: self.context)
                     saveToCoreData.enabled = false
                     saveToCoreData.date = Date()
                     saveToCoreData.duration = 0
                     saveToCoreData.indefinite = false
                     saveToCoreData.percentage = 100
-                    try? self.viewContext.save()
+                    try? self.context.save()
                 }
             }
 
@@ -723,7 +722,7 @@ final class OpenAPS {
     }
 
     func processAndSave(forecastData: [String: [Int]]) {
-        let context = viewContext
+        let context = self.context
 
         let currentDate = Date()
 

+ 1 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -26,7 +26,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settingsManager: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let coredataContext = CoreDataStack.shared.backgroundContext
 
     private enum Config {
         static let filterTime: TimeInterval = 4.5 * 60

+ 12 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -26,7 +26,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         injectServices(resolver)
     }
 
-    private let context = CoreDataStack.shared.viewContext
+    private let context = CoreDataStack.shared.backgroundContext
 
     func storePumpEvents(_ events: [NewPumpEvent]) {
         processQueue.async {
@@ -47,6 +47,17 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         new.external = false
                         new.id = UUID()
                         new.isSMB = true
+
+                        do {
+                            try self.context.save()
+                            debugPrint(
+                                "Pump History storage: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) saved smbs to core data"
+                            )
+                        } catch {
+                            debugPrint(
+                                "Pump History storage: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to save smbs to core data"
+                            )
+                        }
                     }
 
                     return [PumpHistoryEvent(

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

@@ -190,9 +190,13 @@ extension Bolus {
 
                 currentBG = Decimal(lastGlucose)
                 deltaBG = delta
-
+                debugPrint(
+                    "Bolus State: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) fetched glucose"
+                )
             } catch {
-                debugPrint("Bolus State: \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to fetch glucose")
+                debugPrint(
+                    "Bolus State: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to fetch glucose"
+                )
             }
         }
 
@@ -422,6 +426,7 @@ extension Bolus {
             // save to core data asynchronously
             context.perform {
                 let newItem = InsulinStored(context: self.context)
+                newItem.id = UUID()
                 newItem.amount = self.amount as NSDecimalNumber
                 newItem.date = Date()
                 newItem.external = true

+ 20 - 1
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -10,7 +10,7 @@ extension DataTable {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var healthKitManager: HealthKitManager!
 
-        let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
+        let coredataContext = CoreDataStack.shared.viewContext
 
         @Published var mode: Mode = .treatments
         @Published var treatments: [Treatment] = []
@@ -229,6 +229,7 @@ extension DataTable {
 
         func addManualGlucose() {
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
+            let glucoseAsInt = Int(glucose)
             let now = Date()
             let id = UUID().uuidString
 
@@ -248,6 +249,24 @@ extension DataTable {
             // Save to Health
             var saveToHealth = [BloodGlucose]()
             saveToHealth.append(saveToJSON)
+
+            // save to core data
+            let newItem = GlucoseStored(context: coredataContext)
+            newItem.id = UUID()
+            newItem.date = Date()
+            newItem.glucose = Int16(glucoseAsInt)
+            newItem.isManual = true
+
+            do {
+                try coredataContext.save()
+                debugPrint(
+                    "Data table state model: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) added manual glucose to core data"
+                )
+            } catch {
+                debugPrint(
+                    "Data table state model: \(#function) \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to add manual glucose to core data"
+                )
+            }
         }
     }
 }

+ 0 - 2
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -9,14 +9,12 @@ protocol HomeProvider: Provider {
     var suggestion: Suggestion? { get }
     var enactedSuggestion: Suggestion? { get }
     func heartbeatNow()
-    func filteredGlucose(hours: Int) -> [BloodGlucose]
     func pumpHistory(hours: Int) -> [PumpHistoryEvent]
     func pumpSettings() -> PumpSettings
     func autotunedBasalProfile() -> [BasalProfileEntry]
     func basalProfile() -> [BasalProfileEntry]
     func tempTargets(hours: Int) -> [TempTarget]
     func carbs(hours: Int) -> [CarbsEntry]
-    func pumpBattery() -> Battery?
     func pumpReservoir() -> Decimal?
     func tempTarget() -> TempTarget?
     func announcement(_ hours: Int) -> [Announcement]

+ 0 - 10
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -27,12 +27,6 @@ extension Home {
             apsManager.heartbeat(date: Date())
         }
 
-        func filteredGlucose(hours: Int) -> [BloodGlucose] {
-            glucoseStorage.recent().filter {
-                $0.dateString.addingTimeInterval(hours.hours.timeInterval) > Date()
-            }
-        }
-
         func pumpHistory(hours: Int) -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent().filter {
                 $0.timestamp.addingTimeInterval(hours.hours.timeInterval) > Date()
@@ -67,10 +61,6 @@ extension Home {
                 ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2)
         }
 
-        func pumpBattery() -> Battery? {
-            storage.retrieve(OpenAPS.Monitor.battery, as: Battery.self)
-        }
-
         func pumpReservoir() -> Decimal? {
             storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self)
         }

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

@@ -11,12 +11,10 @@ extension Home {
         @Injected() var nightscoutManager: NightscoutManager!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
-        @Published var glucose: [BloodGlucose] = []
         @Published var manualGlucose: [BloodGlucose] = []
         @Published var announcement: [Announcement] = []
         @Published var uploadStats = false
         @Published var recentGlucose: BloodGlucose?
-        @Published var glucoseDelta: Int?
         @Published var tempBasals: [PumpHistoryEvent] = []
         @Published var boluses: [PumpHistoryEvent] = []
         @Published var suspensions: [PumpHistoryEvent] = []
@@ -24,7 +22,6 @@ extension Home {
         @Published var autotunedBasalProfile: [BasalProfileEntry] = []
         @Published var basalProfile: [BasalProfileEntry] = []
         @Published var tempTargets: [TempTarget] = []
-        @Published var carbs: [CarbsEntry] = []
         @Published var timerDate = Date()
         @Published var closedLoop = false
         @Published var pumpSuspended = false
@@ -42,7 +39,6 @@ extension Home {
         @Published var errorDate: Date? = nil
         @Published var bolusProgress: Decimal?
         @Published var eventualBG: Int?
-        @Published var carbsRequired: Decimal?
         @Published var allowManualTemp = false
         @Published var units: GlucoseUnits = .mmolL
         @Published var pumpDisplayState: PumpDisplayState?
@@ -65,33 +61,24 @@ extension Home {
         @Published var tins: Bool = false
         @Published var isTempTargetActive: Bool = false
 
-        @Published var cob: Decimal = 0
         @Published var roundedTotalBolus: String = ""
 
         @Published var selectedTab: Int = 0
 
         @Published var waitForSuggestion: Bool = false
 
-        @Published var carbsForChart: [CarbsEntry] = []
-        @Published var fpusForChart: [CarbsEntry] = []
-
         let context = CoreDataStack.shared.viewContext
 
         override func subscribe() {
-            setupGlucose()
             setupBasals()
             setupBoluses()
             setupSuspensions()
             setupPumpSettings()
             setupBasalProfile()
             setupTempTargets()
-            setupCarbs()
-            setupBattery()
             setupReservoir()
             setupAnnouncements()
             setupCurrentPumpTimezone()
-            filterCarbs()
-            filterFpus()
 
             uploadStats = settingsManager.settings.uploadStats
             units = settingsManager.settings.units
@@ -118,8 +105,6 @@ extension Home {
             broadcaster.register(PumpSettingsObserver.self, observer: self)
             broadcaster.register(BasalProfileObserver.self, observer: self)
             broadcaster.register(TempTargetsObserver.self, observer: self)
-            broadcaster.register(CarbsObserver.self, observer: self)
-            broadcaster.register(PumpBatteryObserver.self, observer: self)
             broadcaster.register(PumpReservoirObserver.self, observer: self)
 
             animatedBackground = settingsManager.settings.animatedBackground
@@ -181,7 +166,6 @@ extension Home {
                         self.pumpExpiresAtDate = nil
                         self.setupPump = false
                     } else {
-                        self.setupBattery()
                         self.setupReservoir()
                     }
                 }
@@ -207,28 +191,6 @@ extension Home {
                 .store(in: &lifetime)
         }
 
-        func filterCarbs() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                let allCarbs = self.provider.carbs(hours: self.filteredHours)
-                let filteredCarbs = allCarbs.filter { !($0.isFPU ?? false) }
-
-                self.carbsForChart.removeAll()
-                self.carbsForChart.append(contentsOf: filteredCarbs)
-            }
-        }
-
-        func filterFpus() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                let allCarbs = self.provider.carbs(hours: self.filteredHours)
-                let filteredFpus = allCarbs.filter { $0.isFPU ?? false }
-
-                self.fpusForChart.removeAll()
-                self.fpusForChart.append(contentsOf: filteredFpus)
-            }
-        }
-
         func runLoop() {
             provider.heartbeatNow()
         }
@@ -249,28 +211,6 @@ extension Home {
             }
         }
 
-        private func setupGlucose() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                let filteredGlucose = self.provider.filteredGlucose(hours: self.filteredHours)
-
-                self.glucose = filteredGlucose
-                self.manualGlucose = filteredGlucose.filter { $0.type == GlucoseType.manual.rawValue }
-
-                self.recentGlucose = self.glucose.last
-
-                if self.glucose.count >= 2 {
-                    self
-                        .glucoseDelta = (self.recentGlucose?.glucose ?? 0) -
-                        (self.glucose[self.glucose.count - 2].glucose ?? 0)
-                } else {
-                    self.glucoseDelta = nil
-                }
-
-                self.alarm = self.provider.glucoseStorage.alarm
-            }
-        }
-
         private func setupBasals() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -359,13 +299,6 @@ extension Home {
             }
         }
 
-        private func setupCarbs() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.carbs = self.provider.carbs(hours: self.filteredHours)
-            }
-        }
-
         private func setupAnnouncements() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -380,13 +313,6 @@ extension Home {
             }
         }
 
-        private func setupBattery() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.battery = self.provider.pumpBattery()
-            }
-        }
-
         private func setupCurrentTempTarget() {
             tempTarget = provider.tempTarget()
         }
@@ -432,13 +358,11 @@ extension Home.StateModel:
     PumpSettingsObserver,
     BasalProfileObserver,
     TempTargetsObserver,
-    CarbsObserver,
-    PumpBatteryObserver,
     PumpReservoirObserver,
     PumpTimeZoneObserver
 {
     func glucoseDidUpdate(_: [BloodGlucose]) {
-        setupGlucose()
+//        setupGlucose()
     }
 
     func determinationDidUpdate(_: Determination) {
@@ -460,8 +384,6 @@ extension Home.StateModel:
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
         tins = settingsManager.settings.tins
-
-        setupGlucose()
     }
 
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
@@ -483,16 +405,6 @@ extension Home.StateModel:
         setupTempTargets()
     }
 
-    func carbsDidUpdate(_: [CarbsEntry]) {
-        setupCarbs()
-        filterFpus()
-        filterCarbs()
-    }
-
-    func pumpBatteryDidChange(_: Battery) {
-        setupBattery()
-    }
-
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
     }

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

@@ -47,9 +47,6 @@ struct MainChartView: View {
         static let minGlucose = 45
     }
 
-    @Binding var glucose: [BloodGlucose]
-    @Binding var manualGlucose: [BloodGlucose]
-    @Binding var fpusForChart: [CarbsEntry]
     @Binding var units: GlucoseUnits
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
@@ -106,11 +103,16 @@ struct MainChartView: View {
     ) var insulinFromPersistence: FetchedResults<InsulinStored>
 
     @FetchRequest(
-        fetchRequest: GlucoseStored.fetch(NSPredicate.predicateForOneDayAgo, ascending: false),
+        fetchRequest: GlucoseStored.fetch(NSPredicate.glucose, ascending: true),
         animation: Animation.bouncy
     ) var glucoseFromPersistence: FetchedResults<GlucoseStored>
 
     @FetchRequest(
+        fetchRequest: GlucoseStored.fetch(NSPredicate.manualGlucose, ascending: true),
+        animation: Animation.bouncy
+    ) var manualGlucoseFromPersistence: FetchedResults<GlucoseStored>
+
+    @FetchRequest(
         fetchRequest: OrefDetermination.fetch(NSPredicate.enactedDetermination),
         animation: Animation.bouncy
     ) var determinations: FetchedResults<OrefDetermination>
@@ -166,7 +168,7 @@ struct MainChartView: View {
         VStack {
             ScrollViewReader { scroller in
                 ScrollView(.horizontal, showsIndicators: false) {
-                    VStack(spacing: 0) {
+                    LazyVStack(spacing: 0) {
                         mainChart
                         basalChart
 
@@ -256,7 +258,7 @@ extension MainChartView {
                 }
             }
             .id("MainChart")
-            .onChange(of: glucose) { _ in
+            .onChange(of: glucoseFromPersistence.map(\.id)) { _ in
 //                calculatePredictions()
             }
             .onChange(of: boluses) { _ in
@@ -278,7 +280,7 @@ extension MainChartView {
             ) { _ in
 //                calculatePredictions()
             }
-            .frame(minHeight: UIScreen.main.bounds.height * 0.2)
+            .frame(minHeight: UIScreen.main.bounds.height * 0.3)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)
             .chartXAxis { mainChartXAxis }
@@ -340,7 +342,7 @@ extension MainChartView {
             }.onChange(of: basalProfile) { _ in
                 calculateTempBasals()
             }
-            .frame(height: UIScreen.main.bounds.height * 0.08)
+            .frame(maxHeight: UIScreen.main.bounds.height * 0.05)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)
             .chartXAxis { basalChartXAxis }
@@ -374,7 +376,7 @@ extension MainChartView {
             let bolusAmount = bolus.amount ?? 0 as NSDecimalNumber
             let bolusDate = bolus.date ?? Date()
             let glucose = timeToNearestGlucose(time: bolusDate.timeIntervalSince1970)
-            let yPosition = (Decimal(glucose.sgv ?? defaultBolusPosition) * conversionFactor) + bolusOffset
+            let yPosition = (Decimal(glucose.glucose) * conversionFactor) + bolusOffset
             let size = (Config.bolusSize + CGFloat(truncating: bolusAmount) * Config.bolusScale) * 1.8
 
             PointMark(
@@ -572,16 +574,15 @@ extension MainChartView {
 
     private func drawManualGlucose() -> some ChartContent {
         /// manual glucose mark
-        ForEach(manualGlucose) { item in
-            if let manualGlucose = item.glucose {
-                PointMark(
-                    x: .value("Time", item.dateString, unit: .second),
-                    y: .value("Value", Decimal(manualGlucose) * conversionFactor)
-                )
-                .symbol {
-                    Image(systemName: "drop.fill").font(.system(size: 10)).symbolRenderingMode(.monochrome)
-                        .foregroundStyle(.red)
-                }
+        ForEach(manualGlucoseFromPersistence) { item in
+            let manualGlucose = item.glucose
+            PointMark(
+                x: .value("Time", item.date ?? Date(), unit: .second),
+                y: .value("Value", Decimal(manualGlucose) * conversionFactor)
+            )
+            .symbol {
+                Image(systemName: "drop.fill").font(.system(size: 10)).symbolRenderingMode(.monochrome)
+                    .foregroundStyle(.red)
             }
         }
     }
@@ -680,27 +681,22 @@ extension MainChartView {
 
     /// calculates the glucose value thats the nearest to parameter 'time'
     /// if time is later than all the arrays values return the last element of BloodGlucose
-    private func timeToNearestGlucose(time: TimeInterval) -> BloodGlucose {
+    private func timeToNearestGlucose(time: TimeInterval) -> GlucoseStored {
         /// If the glucose array is empty, return a default BloodGlucose object or handle it accordingly
-        guard let lastGlucose = glucose.last else {
-            return BloodGlucose(
-                date: 0,
-                dateString: Date(),
-                unfiltered: nil,
-                filtered: nil,
-                noise: nil,
-                type: nil
-            )
+        guard let lastGlucose = glucoseFromPersistence.last else {
+            return GlucoseStored()
         }
 
         /// If the last glucose entry is before the specified time, return the last entry
-        if lastGlucose.dateString.timeIntervalSince1970 < time {
+        if lastGlucose.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970 < time {
             return lastGlucose
         }
 
         /// Find the index of the first element in the array whose date is greater than the specified time
-        if let nextIndex = glucose.firstIndex(where: { $0.dateString.timeIntervalSince1970 > time }) {
-            return glucose[nextIndex]
+        if let nextIndex = glucoseFromPersistence
+            .firstIndex(where: { $0.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970 > time })
+        {
+            return glucoseFromPersistence[nextIndex]
         } else {
             /// If no such element is found, return the last element in the array
             return lastGlucose
@@ -914,7 +910,7 @@ extension MainChartView {
     // MARK: - Chart formatting
 
     private func yAxisChartData() {
-        let glucoseMapped = glucose.compactMap(\.glucose)
+        let glucoseMapped = glucoseFromPersistence.map(\.glucose)
         guard let minGlucose = glucoseMapped.min(), let maxGlucose = glucoseMapped.max() else {
             // default values
             minValue = 45 * conversionFactor - 20 * conversionFactor

+ 12 - 10
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -2,9 +2,7 @@ import CoreData
 import SwiftUI
 
 struct CurrentGlucoseView: View {
-//    @Binding var recentGlucose: BloodGlucose?
     @Binding var timerDate: Date
-    @Binding var delta: Int?
     @Binding var units: GlucoseUnits
     @Binding var alarm: GlucoseAlarm?
     @Binding var lowGlucose: Decimal
@@ -12,11 +10,6 @@ struct CurrentGlucoseView: View {
 
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [
-        // 184, 87, 255
-        // 159, 108, 250
-        // 124, 139, 243
-        // 87, 170, 236
-        // 67, 187, 233
         Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
         Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
         Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
@@ -101,9 +94,6 @@ struct CurrentGlucoseView: View {
 
                     Text(
                         delta
-                            .map {
-                                deltaFormatter.string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
-                            } ?? "--"
                     )
                     .font(.caption2).foregroundColor(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
                 }.frame(alignment: .top)
@@ -146,6 +136,18 @@ struct CurrentGlucoseView: View {
         }
     }
 
+    private var delta: String {
+        guard glucoseFromPersistence.count >= 2 else {
+            return "--"
+        }
+
+        let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
+        let secondLastGlucose = glucoseFromPersistence.dropFirst().first?.glucose ?? 0
+        let delta = lastGlucose - secondLastGlucose
+        let deltaAsDecimal = Decimal(delta)
+        return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
+    }
+
     var colourGlucoseText: Color {
         // Fetch the first glucose reading and convert it to Int for comparison
         let whichGlucose = Int(glucoseFromPersistence.first?.glucose ?? 0)

+ 11 - 5
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -1,8 +1,9 @@
+import CoreData
 import SwiftUI
 
 struct PumpView: View {
     @Binding var reservoir: Decimal?
-    @Binding var battery: Battery?
+//    @Binding var battery: Battery?
     @Binding var name: String
     @Binding var expiresAtDate: Date?
     @Binding var timerDate: Date
@@ -10,6 +11,11 @@ struct PumpView: View {
 
     @State var state: Home.StateModel
 
+    @FetchRequest(
+        fetchRequest: OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo),
+        animation: Animation.bouncy
+    ) var battery: FetchedResults<OpenAPS_Battery>
+
     @Environment(\.colorScheme) var colorScheme
 
     private var reservoirFormatter: NumberFormatter {
@@ -64,12 +70,12 @@ struct PumpView: View {
                 }
             }
 
-            if let battery = battery, battery.display ?? false, expiresAtDate == nil {
+            if (battery.first?.display) != nil, expiresAtDate == nil {
                 HStack {
                     Image(systemName: "battery.100")
                         .font(.system(size: 16))
                         .foregroundColor(batteryColor)
-                    Text("\(Int(battery.percent ?? 100)) %").font(.system(size: 16, design: .rounded))
+                    Text("\(Int(battery.first?.percent ?? 100)) %").font(.system(size: 16, design: .rounded))
                 }
             }
 
@@ -110,11 +116,11 @@ struct PumpView: View {
     }
 
     private var batteryColor: Color {
-        guard let battery = battery, let percent = battery.percent else {
+        guard let battery = battery.first else {
             return .gray
         }
 
-        switch percent {
+        switch battery.percent {
         case ...10:
             return .red
         case ...20:

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

@@ -45,11 +45,16 @@ extension Home {
         @FetchRequest(
             entity: OrefDetermination.entity(),
             sortDescriptors: [NSSortDescriptor(key: "deliverAt", ascending: false)],
-            predicate: NSPredicate.enactedDetermination,
+            predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             animation: Animation.bouncy
         ) var determination: FetchedResults<OrefDetermination>
 
         @FetchRequest(
+            fetchRequest: OrefDetermination.fetch(NSPredicate.enactedDetermination),
+            animation: Animation.bouncy
+        ) var enactedDeterminations: FetchedResults<OrefDetermination>
+
+        @FetchRequest(
             entity: OverridePresets.entity(),
             sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(
                 format: "name != %@", "" as String
@@ -155,7 +160,6 @@ extension Home {
         var glucoseView: some View {
             CurrentGlucoseView(
                 timerDate: $state.timerDate,
-                delta: $state.glucoseDelta,
                 units: $state.units,
                 alarm: $state.alarm,
                 lowGlucose: $state.lowGlucose,
@@ -182,7 +186,6 @@ extension Home {
         var pumpView: some View {
             PumpView(
                 reservoir: $state.reservoir,
-                battery: $state.battery,
                 name: $state.pumpName,
                 expiresAtDate: $state.pumpExpiresAtDate,
                 timerDate: $state.timerDate,
@@ -369,9 +372,6 @@ extension Home {
                 }
 
                 MainChartView(
-                    glucose: $state.glucose,
-                    manualGlucose: $state.manualGlucose,
-                    fpusForChart: $state.fpusForChart,
                     units: $state.units,
                     tempBasals: $state.tempBasals,
                     boluses: $state.boluses,
@@ -870,7 +870,7 @@ extension Home {
             VStack(alignment: .leading, spacing: 4) {
                 Text(statusTitle).font(.headline).foregroundColor(.white)
                     .padding(.bottom, 4)
-                if let determination = determination.first {
+                if let determination = enactedDeterminations.first {
                     TagCloudView(tags: determination.reasonParts).animation(.none, value: false)
 
                     Text(determination.reasonConclusion.capitalizingFirstLetter()).font(.caption).foregroundColor(.white)

+ 1 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift

@@ -15,6 +15,7 @@ struct LiveActivityAttributes: ActivityAttributes {
         let cob: Decimal
         let iob: Decimal
         let lockScreenView: String
+        let unit: String
     }
 
     let startDate: Date

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

@@ -80,6 +80,7 @@ extension LiveActivityAttributes.ContentState {
         let iob = suggestion.iob ?? 0
 
         let lockScreenView = settings.lockScreenView.displayName
+        let unit = settings.units == .mmolL ? " mmol/L" : " mg/dL"
 
         self.init(
             bg: formattedBG,
@@ -93,7 +94,8 @@ extension LiveActivityAttributes.ContentState {
             lowGlucose: Double(lowGlucose),
             cob: cob,
             iob: iob,
-            lockScreenView: lockScreenView
+            lockScreenView: lockScreenView,
+            unit: unit
         )
     }
 }
@@ -230,7 +232,8 @@ extension LiveActivityAttributes.ContentState {
                         lowGlucose: Double(70),
                         cob: 0,
                         iob: 0,
-                        lockScreenView: "Simple"
+                        lockScreenView: "Simple",
+                        unit: "--"
                     ),
                     staleDate: Date.now.addingTimeInterval(60)
                 )

+ 34 - 1
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -61,6 +61,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         return NightscoutAPI(url: url, secret: secret)
     }
 
+    private let context = CoreDataStack.shared.backgroundContext
+
     init(resolver: Resolver) {
         injectServices(resolver)
         subscribe()
@@ -377,6 +379,36 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
+    private func fetchBattery() -> Battery {
+        do {
+            let results = try context.fetch(OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo))
+            if let last = results.first {
+                let percent: Int? = Int(last.percent)
+                let voltage: Decimal? = last.voltage as Decimal?
+                let status: String? = last.status
+                let display: Bool? = last.display
+
+                if let percent = percent, let voltage = voltage, let status = status, let display = display {
+                    debugPrint(
+                        "Home State Model: \(#function) \(DebuggingIdentifiers.succeeded) setup battery from core data successfully"
+                    )
+                    return Battery(
+                        percent: percent,
+                        voltage: voltage,
+                        string: BatteryState(rawValue: status) ?? BatteryState.normal,
+                        display: display
+                    )
+                }
+            }
+            return Battery(percent: 100, voltage: 100, string: BatteryState.normal, display: false)
+        } catch {
+            debugPrint(
+                "Home State Model: \(#function) \(DebuggingIdentifiers.failed) failed to setup battery from core data"
+            )
+            return Battery(percent: 100, voltage: 100, string: BatteryState.normal, display: false)
+        }
+    }
+
     func uploadStatus() {
         let iob = storage.retrieve(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         var suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
@@ -409,7 +441,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             )
         }
 
-        let battery = storage.retrieve(OpenAPS.Monitor.battery, as: Battery.self)
+        let battery = fetchBattery()
+//        let battery = storage.retrieve(OpenAPS.Monitor.battery, as: Battery.self)
 
         var reservoir = Decimal(from: storage.retrieveRaw(OpenAPS.Monitor.reservoir) ?? "0")
         if reservoir == 0xDEAD_BEEF {

+ 1 - 0
GlucoseStored+CoreDataProperties.swift

@@ -10,6 +10,7 @@ public extension GlucoseStored {
     @NSManaged var direction: String?
     @NSManaged var glucose: Int16
     @NSManaged var id: UUID?
+    @NSManaged var isManual: Bool
 }
 
 extension GlucoseStored: Identifiable {}

+ 103 - 61
LiveActivity/LiveActivity.swift

@@ -35,10 +35,12 @@ struct LiveActivity: Widget {
     @ViewBuilder private func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
         if !context.state.change.isEmpty {
             if context.isStale {
-                Text(context.state.change).foregroundStyle(.primary.opacity(0.5))
-                    .strikethrough(pattern: .solid, color: .red.opacity(0.6))
+                Text(context.state.change).foregroundStyle(.primary.opacity(0.5)).font(.headline)
+                    .strikethrough(pattern: .solid, color: .red.opacity(0.6)).font(.callout)
             } else {
-                Text(context.state.change)
+                HStack {
+                    Text(context.state.change).font(.headline)
+                }
             }
         } else {
             Text("--")
@@ -46,22 +48,52 @@ struct LiveActivity: Widget {
     }
 
     @ViewBuilder func mealLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        VStack(alignment: .leading, spacing: 1, content: {
-            HStack {
-                Text("COB: ").font(.caption)
-                Text(
-                    (carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--") +
-                        NSLocalizedString(" g", comment: "grams of carbs")
-                ).font(.caption).fontWeight(.bold)
-            }
-            HStack {
-                Text("IOB: ").font(.caption)
-                Text(
-                    (bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--") +
-                        NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
-                ).font(.caption).fontWeight(.bold)
-            }
-        })
+        HStack {
+            VStack(alignment: .leading, spacing: 1, content: {
+                HStack {
+                    Image(systemName: "fork.knife")
+                        .font(.title3)
+                        .foregroundColor(.yellow)
+                }
+                HStack {
+                    Image(systemName: "syringe.fill")
+                        .font(.title3)
+                        .foregroundColor(.blue)
+                }
+            })
+            VStack(alignment: .trailing, spacing: 1, content: {
+                HStack {
+                    if context.isStale {
+                        Text(
+                            carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--"
+                        ).fontWeight(.bold).font(.headline).strikethrough(pattern: .solid, color: .red.opacity(0.6))
+                            .font(.callout)
+                        Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
+                    } else {
+                        Text(
+                            carbsFormatter.string(from: context.state.cob as NSNumber) ?? "--"
+                        ).fontWeight(.bold).font(.headline)
+                        Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
+                    }
+                }
+                HStack {
+                    if context.isStale {
+                        Text(
+                            bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--"
+                        ).font(.headline).fontWeight(.bold).strikethrough(pattern: .solid, color: .red.opacity(0.6))
+                            .font(.callout)
+                        Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
+                            .foregroundStyle(.secondary).font(.footnote)
+                    } else {
+                        Text(
+                            bolusFormatter.string(from: context.state.iob as NSNumber) ?? "--"
+                        ).font(.headline).fontWeight(.bold)
+                        Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
+                            .foregroundStyle(.secondary).font(.footnote)
+                    }
+                }
+            })
+        }
     }
 
     @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
@@ -76,21 +108,31 @@ struct LiveActivity: Widget {
 
     private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
         let text = Text("Updated: \(dateFormatter.string(from: context.state.date))")
+            .font(.caption2)
         if context.isStale {
+            // foregroundStyle is not available in <iOS 17 hence the check here
             if #available(iOSApplicationExtension 17.0, *) {
                 return text.bold().foregroundStyle(.red)
             } else {
                 return text.bold().foregroundColor(.red)
             }
         } else {
-            return text
+            if #available(iOSApplicationExtension 17.0, *) {
+                return text.bold().foregroundStyle(.secondary)
+            } else {
+                return text.bold().foregroundColor(.red)
+            }
         }
     }
 
-    private func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
-        Text(context.state.bg)
-            .fontWeight(.bold)
-            .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+    @ViewBuilder private func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        HStack(alignment: .center) {
+            Text(context.state.bg)
+                .fontWeight(.bold)
+                .font(.largeTitle)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            Text(context.state.unit).foregroundStyle(.secondary).font(.subheadline).offset(x: -5, y: 5)
+        }
     }
 
     private func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>, size: Size) -> (some View, Int) {
@@ -153,58 +195,66 @@ struct LiveActivity: Widget {
     }
 
     @ViewBuilder func bobble(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        @State var angularGradient = AngularGradient(colors: [
-            Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
+        let gradient = LinearGradient(colors: [
             Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
             Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
             Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
-            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902),
-            Color(red: 0.7215686275, green: 0.3411764706, blue: 1)
-        ], center: .center, startAngle: .degrees(270), endAngle: .degrees(-90))
-        let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
+            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
+        ], startPoint: .leading, endPoint: .trailing)
 
-        WidgetBobble(gradient: angularGradient, color: triangleColor)
-            .rotationEffect(.degrees(context.state.rotationDegrees))
+        if !context.isStale {
+            Image(systemName: "arrow.right")
+                .font(.title)
+                .rotationEffect(.degrees(context.state.rotationDegrees))
+                .foregroundStyle(gradient)
+        }
     }
 
     @ViewBuilder func chart(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
         if context.isStale {
             Text("No data available")
         } else {
+            // determine scale
+            let min = (context.state.chart.min() ?? 40 * (context.state.unit == " mmol/L" ? 0.0555 : 1)) - 20 *
+                (context.state.unit == " mmol/L" ? 0.0555 : 1)
+            let max = (context.state.chart.max() ?? 270 * (context.state.unit == " mmol/L" ? 0.0555 : 1)) + 50 *
+                (context.state.unit == " mmol/L" ? 0.0555 : 1)
+
             Chart {
+                RuleMark(y: .value("high", context.state.highGlucose))
+                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
+                RuleMark(y: .value("low", context.state.lowGlucose))
+                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
                 ForEach(context.state.chart.indices, id: \.self) { index in
                     let currentValue = context.state.chart[index]
                     if currentValue > context.state.highGlucose {
                         PointMark(
                             x: .value("Time", context.state.chartDate[index] ?? Date()),
                             y: .value("Value", currentValue)
-                        ).foregroundStyle(Color.orange.gradient).symbolSize(12)
+                        ).foregroundStyle(Color.orange.gradient).symbolSize(15)
                     } else if currentValue < context.state.lowGlucose {
                         PointMark(
                             x: .value("Time", context.state.chartDate[index] ?? Date()),
                             y: .value("Value", currentValue)
-                        ).foregroundStyle(Color.red.gradient).symbolSize(12)
+                        ).foregroundStyle(Color.red.gradient).symbolSize(15)
                     } else {
                         PointMark(
                             x: .value("Time", context.state.chartDate[index] ?? Date()),
                             y: .value("Value", currentValue)
-                        ).foregroundStyle(Color.green.gradient).symbolSize(12)
+                        ).foregroundStyle(Color.green.gradient).symbolSize(15)
                     }
                 }
-            }.chartPlotStyle { plotContent in
-                plotContent.background(.cyan.opacity(0.1))
             }
             .chartYAxis {
-                AxisMarks(position: .leading) { _ in
-                    AxisValueLabel().foregroundStyle(Color.white)
-                    AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white)
+                AxisMarks(position: .trailing) { _ in
+                    AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
+                    AxisValueLabel().foregroundStyle(Color.secondary).font(.footnote)
                 }
             }
+            .chartYScale(domain: min ... max)
             .chartXAxis {
                 AxisMarks(position: .automatic) { _ in
-                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
-                        .foregroundStyle(Color.white)
-                    AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])).foregroundStyle(Color.white)
+                    AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
                 }
             }
         }
@@ -231,32 +281,24 @@ struct LiveActivity: Widget {
                 .background(BackgroundStyle.background.opacity(0.4))
                 .activityBackgroundTint(Color.clear)
             } else {
-                HStack(spacing: 2) {
-                    VStack {
-                        chart(context: context).frame(width: UIScreen.main.bounds.width / 1.8)
-                    }.padding(.all, 15)
-                    Divider().foregroundStyle(Color.white)
-                    VStack(alignment: .center) {
+                HStack(spacing: 12) {
+                    chart(context: context).frame(width: UIScreen.main.bounds.width / 1.8)
+                    VStack(alignment: .leading) {
                         Spacer()
-                        ZStack {
+                        bgLabel(context: context)
+                        HStack {
+                            changeLabel(context: context)
                             bobble(context: context)
-                                .scaleEffect(0.6)
-                                .clipped()
-                            VStack {
-                                bgLabel(context: context).font(.title2).imageScale(.small)
-                                changeLabel(context: context).font(.callout)
-                            }
-                        }.scaleEffect(0.85).offset(y: 18)
+                        }
                         mealLabel(context: context).padding(.bottom, 8)
-                        updatedLabel(context: context).font(.caption).padding(.bottom, 50)
+                        updatedLabel(context: context).padding(.bottom, 10)
                     }
                 }
                 .privacySensitive()
+                .padding(.all, 14)
                 .imageScale(.small)
-                .background(Color.white.opacity(0.2))
                 .foregroundColor(Color.white)
-                .activityBackgroundTint(Color.black.opacity(0.7))
-                .activitySystemActionForegroundColor(Color.white)
+                .activityBackgroundTint(Color.black)
             }
         } dynamicIsland: { context in
             DynamicIsland {

+ 9 - 0
Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -42,6 +42,7 @@
         <attribute name="direction" optional="YES" attributeType="String"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="isManual" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
     </entity>
     <entity name="HbA1c" representedClassName="HbA1c" syncable="YES">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@@ -89,6 +90,14 @@
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
     </entity>
+    <entity name="OpenAPS_Battery" representedClassName="OpenAPS_Battery" syncable="YES">
+        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="display" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="percent" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="status" optional="YES" attributeType="String"/>
+        <attribute name="voltage" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+    </entity>
     <entity name="Oref0Suggestion" representedClassName="Oref0Suggestion" syncable="YES">
         <relationship name="computedInsulinDistribution" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InsulinDistribution" inverseName="insulin" inverseEntity="InsulinDistribution"/>
         <relationship name="computedTDD" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TDD" inverseName="computed" inverseEntity="TDD"/>

+ 25 - 1
Model/Helper/GlucoseStored+helper.swift

@@ -2,13 +2,25 @@ import CoreData
 import Foundation
 
 extension GlucoseStored {
-    static func fetch(_ predicate: NSPredicate = .all, ascending: Bool) -> NSFetchRequest<GlucoseStored> {
+    static func fetch(_ predicate: NSPredicate = .all, ascending: Bool, fetchLimit: Int? = nil) -> NSFetchRequest<GlucoseStored> {
         let request = GlucoseStored.fetchRequest()
         request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: ascending)]
         request.predicate = predicate
+        if let limit = fetchLimit {
+            request.fetchLimit = limit
+        }
         return request
     }
 
+    static func glucoseIsFlat(_ glucose: [GlucoseStored]) -> Bool {
+        guard glucose.count >= 3 else { return false }
+
+        let lastThreeValues = glucose.suffix(3)
+        let firstValue = lastThreeValues.last?.glucose
+
+        return lastThreeValues.allSatisfy { $0.glucose == firstValue }
+    }
+
 //    static func asyncFetch(_ predicate: NSPredicate = NSPredicate(value: true), completion: @escaping (NSAsynchronousFetchResult<GlucoseStored>)->Void) -> NSAsynchronousFetchRequest<GlucoseStored> {
 //           let request: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
 //           request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: true)]
@@ -22,3 +34,15 @@ extension GlucoseStored {
 //           return asyncFetchRequest
 //       }
 }
+
+extension NSPredicate {
+    static var glucose: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "isManual == %@ AND date >= %@", false as NSNumber, date as NSDate)
+    }
+
+    static var manualGlucose: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "isManual == %@ AND date >= %@", true as NSNumber, date as NSDate)
+    }
+}

+ 3 - 2
Model/Helper/InsulinStored+helper.swift

@@ -4,8 +4,9 @@ import Foundation
 extension InsulinStored {
     static func fetch(_ predicate: NSPredicate = .predicateForOneDayAgo) -> NSFetchRequest<InsulinStored> {
         let request = InsulinStored.fetchRequest()
-        request.sortDescriptors = [NSSortDescriptor(keyPath: \InsulinStored.date, ascending: false)]
-        request.fetchLimit = 100
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \InsulinStored.date, ascending: true)]
+        request.propertiesToFetch = ["amount", "date"]
+        request.resultType = .managedObjectResultType
         request.predicate = predicate
         return request
     }

+ 12 - 0
Model/Helper/OpenAPSBattery.swift

@@ -0,0 +1,12 @@
+import CoreData
+import Foundation
+
+extension OpenAPS_Battery {
+    static func fetch(_ predicate: NSPredicate = .predicateFor30MinAgo) -> NSFetchRequest<OpenAPS_Battery> {
+        let request = OpenAPS_Battery.fetchRequest()
+        request.sortDescriptors = [NSSortDescriptor(keyPath: \OpenAPS_Battery.date, ascending: false)]
+        request.fetchLimit = 1
+        request.predicate = predicate
+        return request
+    }
+}

+ 4 - 0
OpenAPS_Battery+CoreDataClass.swift

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

+ 17 - 0
OpenAPS_Battery+CoreDataProperties.swift

@@ -0,0 +1,17 @@
+import CoreData
+import Foundation
+
+public extension OpenAPS_Battery {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<OpenAPS_Battery> {
+        NSFetchRequest<OpenAPS_Battery>(entityName: "OpenAPS_Battery")
+    }
+
+    @NSManaged var date: Date?
+    @NSManaged var display: Bool
+    @NSManaged var id: UUID?
+    @NSManaged var percent: Int16
+    @NSManaged var status: String?
+    @NSManaged var voltage: NSDecimalNumber?
+}
+
+extension OpenAPS_Battery: Identifiable {}