Przeglądaj źródła

Chart Override (#56)

* Various:
* Fix DecimalTextField
* Fix glucose NS upload
* Fix a ton of typos (thanks Jon & Ivan)
* Add some logging for upload of glucose and treatments to NS

* make glucose upload to ns async and refactor it using an isUploaded Boolean property (no Array subtraction anymore)

* manual Glucose

* carbs

* use UUID in CD entities instead of String, rename Presets to MealPresetStored

* fix overrides be shown as active even after their time is up, remove timer logic from override state, remove observer from ns manager

* Rename isUploaded; start reworking pump events to NS

* Fix faulty predicate; add pumphistory upload to task

* add indicator in LA to show if an Override is active

* fixes for LA

* use correct predicate for LA fetch request

* carb deletion

* upload fpus

* fpu deletion function, to do: fix date of fpus

* pump event deletion

* Migrate external insulin storage to pump history

* manual glucose deletion

* fix update of home view after fpu deletion

* Finish pump event parsing for NS upload; set uploaded flag for PumpEventStored

* Migrate nightscout/Trio#240 fpu conversion; animation broken, fpu date broken WIP

* Fix FPU entry date issue by adding unqiue fpuID

* Change a bunch of iAPS strings to Trio

* increase limit for guard check for stale gluose to 6

* remove suffix from stale glucose check

* overrides in charts

* rename to overrideRunStored

* define relationship

* test relationship, access name property of overrides in chart

* refactoring, improve logic, overrideStorage, schedule deletion

* cleanup

* cleanup

* async rewrite of glucose fetch function

* Fix broken creation for overrides; refactoring and renaming

* Fix override chart shape color

* Rename menu items and change icons

* Some refactoring; add reorder of presets WIP

* small fixes

* fix reorder of Overrides

* Fix active OR not displaying; fix default text not showing when 0 presets

* small refactoring

* fix units not updating

---------

Co-authored-by: Deniz Cengiz <d.c.cengiz@googlemail.com>
polscm32 1 rok temu
rodzic
commit
4aa3e9f109
58 zmienionych plików z 2104 dodań i 1565 usunięć
  1. 2 0
      CarbEntryStored+CoreDataProperties.swift
  2. 60 40
      FreeAPS.xcodeproj/project.pbxproj
  3. 2 2
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  4. 5 8
      FreeAPS/Sources/APS/APSManager.swift
  5. 6 2
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  6. 284 135
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  7. 67 89
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  8. 180 0
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  9. 200 221
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  10. 4 4
      FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift
  11. 2 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  12. 1 0
      FreeAPS/Sources/Assemblies/StorageAssembly.swift
  13. 1 1
      FreeAPS/Sources/Models/AlertEntry.swift
  14. 3 3
      FreeAPS/Sources/Models/BloodGlucose.swift
  15. 1 1
      FreeAPS/Sources/Models/CarbsEntry.swift
  16. 6 6
      FreeAPS/Sources/Models/NightscoutTreatment.swift
  17. 24 0
      FreeAPS/Sources/Models/Override.swift
  18. 1 1
      FreeAPS/Sources/Models/TempTarget.swift
  19. 11 53
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  20. 6 6
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  21. 2 2
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  22. 26 10
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  23. 39 31
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  24. 136 1
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  25. 44 0
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  26. 12 5
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  27. 11 9
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  28. 6 2
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  29. 3 3
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesDataFlow.swift
  30. 186 360
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  31. 30 29
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/AddProfileForm.swift
  32. 68 64
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/EditProfileForm.swift
  33. 60 37
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift
  34. 1 1
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  35. 34 17
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  36. 5 0
      FreeAPS/Sources/Services/LiveActivity/Data/OverrideData.swift
  37. 5 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  38. 1 0
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes.swift
  39. 9 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  40. 127 73
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  41. 294 223
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  42. 4 0
      FreeAPS/Sources/Views/DecimalTextField.swift
  43. 1 0
      GlucoseStored+CoreDataProperties.swift
  44. 13 1
      LiveActivity/LiveActivity.swift
  45. 4 0
      MealPresetStored+CoreDataClass.swift
  46. 15 0
      MealPresetStored+CoreDataProperties.swift
  47. 2 2
      Model/CoreDataStack.swift
  48. 29 9
      Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  49. 20 0
      Model/Helper/CarbEntryStored+helper.swift
  50. 18 4
      Model/Helper/GlucoseStored+helper.swift
  51. 7 2
      Model/Helper/PumpEvent+helper.swift
  52. 4 0
      OverrideRunStored+CoreDataClass.swift
  53. 16 0
      OverrideRunStored+CoreDataProperties.swift
  54. 3 6
      OverrideStored+CoreDataProperties.swift
  55. 0 4
      Presets+CoreDataClass.swift
  56. 0 15
      Presets+CoreDataProperties.swift
  57. 0 72
      PumpEventStored+CoreDataClass.swift
  58. 3 7
      PumpEventStored+CoreDataProperties.swift

+ 2 - 0
CarbEntryStored+CoreDataProperties.swift

@@ -11,8 +11,10 @@ public extension CarbEntryStored {
     @NSManaged var fat: Double
     @NSManaged var id: UUID?
     @NSManaged var isFPU: Bool
+    @NSManaged var fpuID: UUID?
     @NSManaged var note: String?
     @NSManaged var protein: Double
+    @NSManaged var isUploadedToNS: Bool
 }
 
 extension CarbEntryStored: Identifiable {}

+ 60 - 40
FreeAPS.xcodeproj/project.pbxproj

@@ -272,22 +272,16 @@
 		5825D13D2BD4058F00F36E9B /* ImportError+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D10F2BD4058F00F36E9B /* ImportError+CoreDataProperties.swift */; };
 		5825D1402BD4058F00F36E9B /* TempTargetsSlider+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1122BD4058F00F36E9B /* TempTargetsSlider+CoreDataClass.swift */; };
 		5825D1412BD4058F00F36E9B /* TempTargetsSlider+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1132BD4058F00F36E9B /* TempTargetsSlider+CoreDataProperties.swift */; };
-		5825D1422BD4058F00F36E9B /* Presets+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1142BD4058F00F36E9B /* Presets+CoreDataClass.swift */; };
-		5825D1432BD4058F00F36E9B /* Presets+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1152BD4058F00F36E9B /* Presets+CoreDataProperties.swift */; };
 		5825D14A2BD4058F00F36E9B /* OrefDetermination+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D11C2BD4058F00F36E9B /* OrefDetermination+CoreDataClass.swift */; };
 		5825D14B2BD4058F00F36E9B /* OrefDetermination+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D11D2BD4058F00F36E9B /* OrefDetermination+CoreDataProperties.swift */; };
 		5825D14C2BD4058F00F36E9B /* TempTargets+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D11E2BD4058F00F36E9B /* TempTargets+CoreDataClass.swift */; };
 		5825D14D2BD4058F00F36E9B /* TempTargets+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D11F2BD4058F00F36E9B /* TempTargets+CoreDataProperties.swift */; };
-		5825D1542BD4058F00F36E9B /* CarbEntryStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1262BD4058F00F36E9B /* CarbEntryStored+CoreDataClass.swift */; };
-		5825D1552BD4058F00F36E9B /* CarbEntryStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1272BD4058F00F36E9B /* CarbEntryStored+CoreDataProperties.swift */; };
 		5825D1582BD4058F00F36E9B /* StatsData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D12A2BD4058F00F36E9B /* StatsData+CoreDataClass.swift */; };
 		5825D1592BD4058F00F36E9B /* StatsData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D12B2BD4058F00F36E9B /* StatsData+CoreDataProperties.swift */; };
 		582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582FAE422C05102C00D1C13F /* CoreDataError.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 /* CarbEntryStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+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 */; };
 		585E2CAE2BE7BF46006ECF1A /* PumpEvent+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E2CAD2BE7BF46006ECF1A /* PumpEvent+helper.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
@@ -354,11 +348,22 @@
 		BDB3C1042C0341E600CEEAA1 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */; };
 		BDB3C1052C0341E600CEEAA1 /* TempBasalStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
+		BDBAACEC2C2C2E8700370AAE /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACEA2C2C2E8700370AAE /* GlucoseStored+CoreDataClass.swift */; };
+		BDBAACED2C2C2E8700370AAE /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACEB2C2C2E8700370AAE /* GlucoseStored+CoreDataProperties.swift */; };
+		BDBAACF12C2C520400370AAE /* CarbEntryStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACEF2C2C520400370AAE /* CarbEntryStored+CoreDataClass.swift */; };
+		BDBAACF22C2C520400370AAE /* CarbEntryStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF02C2C520400370AAE /* CarbEntryStored+CoreDataProperties.swift */; };
+		BDBAACF52C2C9CAE00370AAE /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF32C2C9CAE00370AAE /* MealPresetStored+CoreDataClass.swift */; };
+		BDBAACF62C2C9CAE00370AAE /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF42C2C9CAE00370AAE /* MealPresetStored+CoreDataProperties.swift */; };
+		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
+		BDC2EA402C2FF34400E5BBD0 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA3C2C2FF34400E5BBD0 /* OverrideStored+CoreDataClass.swift */; };
+		BDC2EA412C2FF34400E5BBD0 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA3D2C2FF34400E5BBD0 /* OverrideStored+CoreDataProperties.swift */; };
+		BDC2EA422C2FF34400E5BBD0 /* OverrideRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA3E2C2FF34400E5BBD0 /* OverrideRunStored+CoreDataClass.swift */; };
+		BDC2EA432C2FF34400E5BBD0 /* OverrideRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA3F2C2FF34400E5BBD0 /* OverrideRunStored+CoreDataProperties.swift */; };
+		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
+		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
 		BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */; };
-		BDCD47BA2C2203A600F8BCD5 /* EditProfileForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47B92C2203A600F8BCD5 /* EditProfileForm.swift */; };
-		BDCD47C32C26331400F8BCD5 /* AddProfileForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47C22C26331400F8BCD5 /* AddProfileForm.swift */; };
-		BDCD47DE2C2861F400F8BCD5 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47DC2C2861F400F8BCD5 /* OverrideStored+CoreDataClass.swift */; };
-		BDCD47DF2C2861F400F8BCD5 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47DD2C2861F400F8BCD5 /* OverrideStored+CoreDataProperties.swift */; };
+		BDCD47BA2C2203A600F8BCD5 /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47B92C2203A600F8BCD5 /* EditOverrideForm.swift */; };
+		BDCD47C32C26331400F8BCD5 /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47C22C26331400F8BCD5 /* AddOverrideForm.swift */; };
 		BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */; };
 		BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34F822C10C5B600D51995 /* DataManager.swift */; };
 		BDF34F852C10C62E00D51995 /* GlucoseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34F842C10C62E00D51995 /* GlucoseData.swift */; };
@@ -878,22 +883,16 @@
 		5825D10F2BD4058F00F36E9B /* ImportError+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImportError+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		5825D1122BD4058F00F36E9B /* TempTargetsSlider+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetsSlider+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		5825D1132BD4058F00F36E9B /* TempTargetsSlider+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetsSlider+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
-		5825D1142BD4058F00F36E9B /* Presets+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Presets+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
-		5825D1152BD4058F00F36E9B /* Presets+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Presets+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		5825D11C2BD4058F00F36E9B /* OrefDetermination+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrefDetermination+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		5825D11D2BD4058F00F36E9B /* OrefDetermination+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrefDetermination+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		5825D11E2BD4058F00F36E9B /* TempTargets+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargets+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		5825D11F2BD4058F00F36E9B /* TempTargets+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargets+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
-		5825D1262BD4058F00F36E9B /* CarbEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
-		5825D1272BD4058F00F36E9B /* CarbEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		5825D12A2BD4058F00F36E9B /* StatsData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsData+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		5825D12B2BD4058F00F36E9B /* StatsData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsData+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		582FAE422C05102C00D1C13F /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = "<group>"; };
 		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 /* CarbEntryStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+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; };
 		585E2CAD2BE7BF46006ECF1A /* PumpEvent+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpEvent+helper.swift"; sourceTree = "<group>"; };
 		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>"; };
@@ -961,11 +960,22 @@
 		BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
+		BDBAACEA2C2C2E8700370AAE /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		BDBAACEB2C2C2E8700370AAE /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDBAACEF2C2C520400370AAE /* CarbEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		BDBAACF02C2C520400370AAE /* CarbEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDBAACF32C2C9CAE00370AAE /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		BDBAACF42C2C9CAE00370AAE /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
+		BDC2EA3C2C2FF34400E5BBD0 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		BDC2EA3D2C2FF34400E5BBD0 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDC2EA3E2C2FF34400E5BBD0 /* OverrideRunStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		BDC2EA3F2C2FF34400E5BBD0 /* OverrideRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
+		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
 		BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+helper.swift"; sourceTree = "<group>"; };
-		BDCD47B92C2203A600F8BCD5 /* EditProfileForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileForm.swift; sourceTree = "<group>"; };
-		BDCD47C22C26331400F8BCD5 /* AddProfileForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileForm.swift; sourceTree = "<group>"; };
-		BDCD47DC2C2861F400F8BCD5 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
-		BDCD47DD2C2861F400F8BCD5 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDCD47B92C2203A600F8BCD5 /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
+		BDCD47C22C26331400F8BCD5 /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotification.swift; sourceTree = "<group>"; };
 		BDF34F822C10C5B600D51995 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = "<group>"; };
 		BDF34F842C10C62E00D51995 /* GlucoseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseData.swift; sourceTree = "<group>"; };
@@ -1275,8 +1285,8 @@
 			isa = PBXGroup;
 			children = (
 				19DC678429CA67A400FD9EC4 /* OverrideProfilesRootView.swift */,
-				BDCD47B92C2203A600F8BCD5 /* EditProfileForm.swift */,
-				BDCD47C22C26331400F8BCD5 /* AddProfileForm.swift */,
+				BDCD47B92C2203A600F8BCD5 /* EditOverrideForm.swift */,
+				BDCD47C22C26331400F8BCD5 /* AddOverrideForm.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1816,6 +1826,7 @@
 				CC41E2992B1E1F460070974F /* HistoryLayout.swift */,
 				BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */,
 				583684072BD195A700070A60 /* Determination.swift */,
+				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1864,6 +1875,7 @@
 				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
 				38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
 				CE82E02428E867BA00473A9C /* AlertStorage.swift */,
+				BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */,
 			);
 			path = Storage;
 			sourceTree = "<group>";
@@ -2124,8 +2136,16 @@
 		5825D1052BD4056700F36E9B /* Classes+Properties */ = {
 			isa = PBXGroup;
 			children = (
-				BDCD47DC2C2861F400F8BCD5 /* OverrideStored+CoreDataClass.swift */,
-				BDCD47DD2C2861F400F8BCD5 /* OverrideStored+CoreDataProperties.swift */,
+				BDC2EA3C2C2FF34400E5BBD0 /* OverrideStored+CoreDataClass.swift */,
+				BDC2EA3D2C2FF34400E5BBD0 /* OverrideStored+CoreDataProperties.swift */,
+				BDC2EA3E2C2FF34400E5BBD0 /* OverrideRunStored+CoreDataClass.swift */,
+				BDC2EA3F2C2FF34400E5BBD0 /* OverrideRunStored+CoreDataProperties.swift */,
+				BDBAACF32C2C9CAE00370AAE /* MealPresetStored+CoreDataClass.swift */,
+				BDBAACF42C2C9CAE00370AAE /* MealPresetStored+CoreDataProperties.swift */,
+				BDBAACEF2C2C520400370AAE /* CarbEntryStored+CoreDataClass.swift */,
+				BDBAACF02C2C520400370AAE /* CarbEntryStored+CoreDataProperties.swift */,
+				BDBAACEA2C2C2E8700370AAE /* GlucoseStored+CoreDataClass.swift */,
+				BDBAACEB2C2C2E8700370AAE /* GlucoseStored+CoreDataProperties.swift */,
 				BDB3C0FA2C0341E500CEEAA1 /* BolusStored+CoreDataClass.swift */,
 				BDB3C0FB2C0341E500CEEAA1 /* BolusStored+CoreDataProperties.swift */,
 				BDB3C0FC2C0341E500CEEAA1 /* PumpEventStored+CoreDataClass.swift */,
@@ -2136,8 +2156,6 @@
 				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 */,
 				5825D10C2BD4058F00F36E9B /* LoopStatRecord+CoreDataClass.swift */,
@@ -2146,14 +2164,10 @@
 				5825D10F2BD4058F00F36E9B /* ImportError+CoreDataProperties.swift */,
 				5825D1122BD4058F00F36E9B /* TempTargetsSlider+CoreDataClass.swift */,
 				5825D1132BD4058F00F36E9B /* TempTargetsSlider+CoreDataProperties.swift */,
-				5825D1142BD4058F00F36E9B /* Presets+CoreDataClass.swift */,
-				5825D1152BD4058F00F36E9B /* Presets+CoreDataProperties.swift */,
 				5825D11C2BD4058F00F36E9B /* OrefDetermination+CoreDataClass.swift */,
 				5825D11D2BD4058F00F36E9B /* OrefDetermination+CoreDataProperties.swift */,
 				5825D11E2BD4058F00F36E9B /* TempTargets+CoreDataClass.swift */,
 				5825D11F2BD4058F00F36E9B /* TempTargets+CoreDataProperties.swift */,
-				5825D1262BD4058F00F36E9B /* CarbEntryStored+CoreDataClass.swift */,
-				5825D1272BD4058F00F36E9B /* CarbEntryStored+CoreDataProperties.swift */,
 				5825D12A2BD4058F00F36E9B /* StatsData+CoreDataClass.swift */,
 				5825D12B2BD4058F00F36E9B /* StatsData+CoreDataProperties.swift */,
 			);
@@ -2344,6 +2358,7 @@
 				BDF34F822C10C5B600D51995 /* DataManager.swift */,
 				BDF34F842C10C62E00D51995 /* GlucoseData.swift */,
 				BDF34F942C10D27300D51995 /* DeterminationData.swift */,
+				BDBAACF92C2D439700370AAE /* OverrideData.swift */,
 			);
 			path = Data;
 			sourceTree = "<group>";
@@ -2869,7 +2884,6 @@
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				5825D1402BD4058F00F36E9B /* TempTargetsSlider+CoreDataClass.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicDataFlow.swift in Sources */,
-				5825D1542BD4058F00F36E9B /* CarbEntryStored+CoreDataClass.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -2877,6 +2891,8 @@
 				19E1F7EF29D08EBA005C8D20 /* IconConfigRootWiew.swift in Sources */,
 				1967DFC229D053D300759F30 /* IconImage.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
+				BDBAACEC2C2C2E8700370AAE /* GlucoseStored+CoreDataClass.swift in Sources */,
+				BDBAACED2C2C2E8700370AAE /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
@@ -2899,7 +2915,7 @@
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
 				CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */,
-				BDCD47C32C26331400F8BCD5 /* AddProfileForm.swift in Sources */,
+				BDCD47C32C26331400F8BCD5 /* AddOverrideForm.swift in Sources */,
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */,
 				384E803825C388640086DB71 /* Script.swift in Sources */,
@@ -2920,6 +2936,7 @@
 				5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
+				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
@@ -2932,8 +2949,14 @@
 				19DC678529CA67A400FD9EC4 /* OverrideProfilesRootView.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				CC76E94C2BD471BA008BEB61 /* Forecast+CoreDataClass.swift in Sources */,
+				BDBAACF52C2C9CAE00370AAE /* MealPresetStored+CoreDataClass.swift in Sources */,
+				BDBAACF62C2C9CAE00370AAE /* MealPresetStored+CoreDataProperties.swift in Sources */,
 				CC76E94D2BD471BA008BEB61 /* Forecast+CoreDataProperties.swift in Sources */,
 				CC76E94E2BD471BA008BEB61 /* ForecastValue+CoreDataClass.swift in Sources */,
+				BDC2EA402C2FF34400E5BBD0 /* OverrideStored+CoreDataClass.swift in Sources */,
+				BDC2EA412C2FF34400E5BBD0 /* OverrideStored+CoreDataProperties.swift in Sources */,
+				BDC2EA422C2FF34400E5BBD0 /* OverrideRunStored+CoreDataClass.swift in Sources */,
+				BDC2EA432C2FF34400E5BBD0 /* OverrideRunStored+CoreDataProperties.swift in Sources */,
 				CC76E94F2BD471BA008BEB61 /* ForecastValue+CoreDataProperties.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
@@ -2949,7 +2972,7 @@
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
-				BDCD47BA2C2203A600F8BCD5 /* EditProfileForm.swift in Sources */,
+				BDCD47BA2C2203A600F8BCD5 /* EditOverrideForm.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
 				190EBCC629FF138000BA767D /* StatConfigProvider.swift in Sources */,
 				38E98A2725F52C9300C0CED0 /* CollectionIssueReporter.swift in Sources */,
@@ -3004,7 +3027,6 @@
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
-				5825D1422BD4058F00F36E9B /* Presets+CoreDataClass.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				583684082BD195A700070A60 /* Determination.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
@@ -3084,7 +3106,6 @@
 				CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */,
 				F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */,
 				19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */,
-				5825D1432BD4058F00F36E9B /* Presets+CoreDataProperties.swift in Sources */,
 				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
 				8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */,
 				195D80B42AF6973A00D25097 /* DynamicRootView.swift in Sources */,
@@ -3100,17 +3121,18 @@
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
-				5825D1552BD4058F00F36E9B /* CarbEntryStored+CoreDataProperties.swift in Sources */,
 				195D80BB2AF6980B00D25097 /* DynamicStateModel.swift in Sources */,
 				E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */,
+				BDBAACF12C2C520400370AAE /* CarbEntryStored+CoreDataClass.swift in Sources */,
+				BDBAACF22C2C520400370AAE /* CarbEntryStored+CoreDataProperties.swift in Sources */,
 				5825D1412BD4058F00F36E9B /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				38192E07261BA9960094D973 /* FetchTreatmentsManager.swift in Sources */,
 				19012CDC291D2CB900FB8210 /* LoopStats.swift in Sources */,
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
+				BDC2EA472C3045AD00E5BBD0 /* Override.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 */,
@@ -3156,12 +3178,12 @@
 				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 */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
+				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
@@ -3171,8 +3193,6 @@
 				BDF34F852C10C62E00D51995 /* GlucoseData.swift in Sources */,
 				3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */,
 				19E1F7EC29D082FE005C8D20 /* IconConfigStateModel.swift in Sources */,
-				BDCD47DE2C2861F400F8BCD5 /* OverrideStored+CoreDataClass.swift in Sources */,
-				BDCD47DF2C2861F400F8BCD5 /* OverrideStored+CoreDataProperties.swift in Sources */,
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
 				588752842BD9986A008B081D /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,

+ 2 - 2
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -69,8 +69,8 @@
         "repositoryURL": "https://github.com/Swinject/Swinject",
         "state": {
           "branch": null,
-          "revision": "3125943807991bc271d366205d98713696d65a1f",
-          "version": "2.8.8"
+          "revision": "be9dbcc7b86811bc131539a20c6f9c2d3e56919f",
+          "version": "2.9.1"
         }
       }
     ]

+ 5 - 8
FreeAPS/Sources/APS/APSManager.swift

@@ -345,7 +345,7 @@ final class BaseAPSManager: APSManager, Injectable {
         debug(.apsManager, "Start determine basal")
 
         // Fetch glucose asynchronously
-        let glucose = await fetchGlucose(predicate: NSPredicate.predicateFor30MinAgo, fetchLimit: 4)
+        let glucose = await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
 
         // Perform the context-related checks and actions
         let isValidGlucoseData = await privateContext.perform {
@@ -362,13 +362,10 @@ final class BaseAPSManager: APSManager, Injectable {
                 return false
             }
 
-            // Only let glucose be flat when 400 mg/dl
-            if (glucose.first?.glucose ?? 100) != 400 {
-                guard !GlucoseStored.glucoseIsFlat(glucose) else {
-                    debug(.apsManager, "Glucose data is too flat")
-                    self.processError(APSError.glucoseError(message: "Glucose data is too flat"))
-                    return false
-                }
+            guard !GlucoseStored.glucoseIsFlat(glucose) else {
+                debug(.apsManager, "Glucose data is too flat")
+                self.processError(APSError.glucoseError(message: "Glucose data is too flat"))
+                return false
             }
 
             return true

+ 6 - 2
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -114,12 +114,14 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             guard let results = fetchGlucose() else { return [] }
             return results.map { result in
                 BloodGlucose(
+                    sgv: Int(result.glucose),
+                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
                     dateString: result.date ?? Date(),
                     unfiltered: Decimal(result.glucose),
                     filtered: Decimal(result.glucose),
                     noise: nil,
-                    type: ""
+                    glucose: Int(result.glucose)
                 )
             }
         }
@@ -177,7 +179,9 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
         deviceDataManager.heartbeat(date: Date())
 
-        nightscoutManager.uploadGlucose()
+        Task.detached {
+            await self.nightscoutManager.uploadGlucose()
+        }
 
         let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
 

+ 284 - 135
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -11,7 +11,8 @@ protocol CarbsStorage {
     func storeCarbs(_ carbs: [CarbsEntry])
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
+    func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
 }
 
@@ -29,120 +30,226 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
     func storeCarbs(_ entries: [CarbsEntry]) {
         processQueue.sync {
-            self.handleFPUCalculations(entries: entries)
-            self.storeNormalCarbs(entries: entries)
+            self.storeCarbEquivalents(entries: entries)
             self.saveCarbsToCoreData(entries: entries)
-            self.notifyObservers(entries: entries)
         }
     }
 
-    private func handleFPUCalculations(entries: [CarbsEntry]) {
-        let file = OpenAPS.Monitor.carbHistory
-        var uniqEvents: [CarbsEntry] = []
-
-        let fat = entries.last?.fat ?? 0
-        let protein = entries.last?.protein ?? 0
-
-        if fat > 0 || protein > 0 {
-            // -------------------------- FPU--------------------------------------
-            let interval = settings.settings.minuteInterval // Interval betwwen carbs
-            let timeCap = settings.settings.timeCap // Max Duration
-            let adjustment = settings.settings.individualAdjustmentFactor
-            let delay = settings.settings.delay // Tme before first future carb entry
-            let kcal = protein * 4 + fat * 9
-            let carbEquivalents = (kcal / 10) * adjustment
-            let fpus = carbEquivalents / 10
-            // Duration in hours used for extended boluses with Warsaw Method. Here used for total duration of the computed carbquivalents instead, excluding the configurable delay.
-            var computedDuration = 0
-            switch fpus {
-            case ..<2:
-                computedDuration = 3
-            case 2 ..< 3:
-                computedDuration = 4
-            case 3 ..< 4:
-                computedDuration = 5
-            default:
-                computedDuration = timeCap
-            }
-            // Size of each created carb equivalent if 60 minutes interval
-            var equivalent: Decimal = carbEquivalents / Decimal(computedDuration)
-            // Adjust for interval setting other than 60 minutes
-            equivalent /= Decimal(60 / interval)
-            // Round to 1 fraction digit
-            // equivalent = Decimal(round(Double(equivalent * 10) / 10))
-            let roundedEquivalent: Double = round(Double(equivalent * 10)) / 10
-            equivalent = Decimal(roundedEquivalent)
-            // Number of equivalents
-            var numberOfEquivalents = carbEquivalents / equivalent
-            // Only use delay in first loop
-            var firstIndex = true
-            // New date for each carb equivalent
-            var useDate = entries.last?.actualDate ?? Date()
-            // Group and Identify all FPUs together
-            let fpuID = entries.last?.fpuID ?? ""
-            // Create an array of all future carb equivalents.
-            var futureCarbArray = [CarbsEntry]()
-            while carbEquivalents > 0, numberOfEquivalents > 0 {
-                if firstIndex {
-                    useDate = useDate.addingTimeInterval(delay.minutes.timeInterval)
-                    firstIndex = false
-                } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
-
-                let eachCarbEntry = CarbsEntry(
-                    id: UUID().uuidString, createdAt: entries.last?.createdAt ?? Date(), actualDate: useDate,
-                    carbs: equivalent, fat: 0, protein: 0, note: nil,
-                    enteredBy: CarbsEntry.manual, isFPU: true,
-                    fpuID: fpuID
-                )
-                futureCarbArray.append(eachCarbEntry)
-                numberOfEquivalents -= 1
-            }
-            // Save the array
-            if carbEquivalents > 0 {
-                storage.transaction { storage in
-                    storage.append(futureCarbArray, to: file, uniqBy: \.id)
-                    uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
-                        .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
-                        .sorted { $0.createdAt > $1.createdAt } ?? []
-                    storage.save(Array(uniqEvents), as: file)
-                }
+    /**
+     Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap.
 
-                // MARK: - save also to core data
+     - The function uses predefined rules to determine the duration based on the number of FPUs.
+     - Ensures that the duration does not exceed the time cap.
 
-                saveFPUToCoreDataAsBatchInsert(entries: futureCarbArray)
-            }
+     - Parameters:
+       - fpus: The number of FPUs calculated from fat and protein.
+       - timeCap: The maximum allowed duration.
+
+     - Returns: The computed duration in hours.
+     */
+    private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
+        switch fpus {
+        case ..<2:
+            return 3
+        case 2 ..< 3:
+            return 4
+        case 3 ..< 4:
+            return 5
+        default:
+            return timeCap
+        }
+    }
+
+    /**
+     Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams.
+
+     - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams.
+     - Creates future carb entries based on the adjusted carb equivalent size and interval.
+
+     - Parameters:
+       - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed.
+       - fat: The amount of fat in the last entry.
+       - protein: The amount of protein in the last entry.
+       - createdAt: The creation date of the last entry.
+
+     - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
+     */
+    private func processFPU(entries _: [CarbsEntry], fat: Decimal, protein: Decimal, createdAt: Date) -> ([CarbsEntry], Decimal) {
+        let interval = settings.settings.minuteInterval
+        let timeCap = settings.settings.timeCap
+        let adjustment = settings.settings.individualAdjustmentFactor
+        let delay = settings.settings.delay
+
+        let kcal = protein * 4 + fat * 9
+        let carbEquivalents = (kcal / 10) * adjustment
+        let fpus = carbEquivalents / 10
+        var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
+
+        var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
+        carbEquivalentSize /= Decimal(60 / interval)
+
+        if carbEquivalentSize < 1.0 {
+            carbEquivalentSize = 1.0
+            computedDuration = Int(carbEquivalents / carbEquivalentSize)
+        }
+
+        let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
+        carbEquivalentSize = Decimal(roundedEquivalent)
+        var numberOfEquivalents = carbEquivalents / carbEquivalentSize
+
+        var useDate = createdAt
+        let fpuID = UUID().uuidString
+        var futureCarbArray = [CarbsEntry]()
+        var firstIndex = true
+
+        while carbEquivalents > 0, numberOfEquivalents > 0 {
+            useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
+                .addingTimeInterval(interval.minutes.timeInterval)
+            firstIndex = false
+
+            let eachCarbEntry = CarbsEntry(
+                id: UUID().uuidString,
+                createdAt: createdAt,
+                actualDate: useDate,
+                carbs: carbEquivalentSize,
+                fat: 0,
+                protein: 0,
+                note: nil,
+                enteredBy: CarbsEntry.manual, isFPU: true,
+                fpuID: fpuID
+            )
+            futureCarbArray.append(eachCarbEntry)
+            numberOfEquivalents -= 1
         }
+
+        return (futureCarbArray, carbEquivalents)
     }
 
-    private func storeNormalCarbs(entries: [CarbsEntry]) {
-        let file = OpenAPS.Monitor.carbHistory
-        var uniqEvents: [CarbsEntry] = []
-
-        if let entry = entries.last, entry.carbs > 0 {
-            // uniqEvents = []
-            let onlyCarbs = CarbsEntry(
-                id: entry.id ?? "",
-                createdAt: entry.createdAt,
-                actualDate: entry.actualDate ?? entry.createdAt,
-                carbs: entry.carbs,
-                fat: nil,
-                protein: nil,
-                note: entry.note ?? "",
-                enteredBy: entry.enteredBy ?? "",
-                isFPU: false,
-                fpuID: ""
+    private func storeCarbEquivalents(entries: [CarbsEntry]) {
+        guard let lastEntry = entries.last else { return }
+
+        if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
+            let (futureCarbEquivalents, carbEquivalentCount) = processFPU(
+                entries: entries,
+                fat: fat,
+                protein: protein,
+                createdAt: lastEntry.createdAt
             )
 
-            storage.transaction { storage in
-                storage.append(onlyCarbs, to: file, uniqBy: \.id)
-                uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
-                    .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
-                    .sorted { $0.createdAt > $1.createdAt } ?? []
-                storage.save(Array(uniqEvents), as: file)
+            if carbEquivalentCount > 0 {
+                saveFPUToCoreDataAsBatchInsert(entries: futureCarbEquivalents)
             }
         }
     }
 
+//
+//    private func handleFPUCalculations(entries: [CarbsEntry]) {
+//        let file = OpenAPS.Monitor.carbHistory
+//        var uniqEvents: [CarbsEntry] = []
+//
+//        let fat = entries.last?.fat ?? 0
+//        let protein = entries.last?.protein ?? 0
+//
+//        if fat > 0 || protein > 0 {
+//            // -------------------------- FPU--------------------------------------
+//            let interval = settings.settings.minuteInterval // Interval betwwen carbs
+//            let timeCap = settings.settings.timeCap // Max Duration
+//            let adjustment = settings.settings.individualAdjustmentFactor
+//            let delay = settings.settings.delay // Tme before first future carb entry
+//            let kcal = protein * 4 + fat * 9
+//            let carbEquivalents = (kcal / 10) * adjustment
+//            let fpus = carbEquivalents / 10
+//            // Duration in hours used for extended boluses with Warsaw Method. Here used for total duration of the computed carbquivalents instead, excluding the configurable delay.
+//            var computedDuration = 0
+//            switch fpus {
+//            case ..<2:
+//                computedDuration = 3
+//            case 2 ..< 3:
+//                computedDuration = 4
+//            case 3 ..< 4:
+//                computedDuration = 5
+//            default:
+//                computedDuration = timeCap
+//            }
+//            // Size of each created carb equivalent if 60 minutes interval
+//            var equivalent: Decimal = carbEquivalents / Decimal(computedDuration)
+//            // Adjust for interval setting other than 60 minutes
+//            equivalent /= Decimal(60 / interval)
+//            // Round to 1 fraction digit
+//            // equivalent = Decimal(round(Double(equivalent * 10) / 10))
+//            let roundedEquivalent: Double = round(Double(equivalent * 10)) / 10
+//            equivalent = Decimal(roundedEquivalent)
+//            // Number of equivalents
+//            var numberOfEquivalents = carbEquivalents / equivalent
+//            // Only use delay in first loop
+//            var firstIndex = true
+//            // New date for each carb equivalent
+//            var useDate = entries.last?.actualDate ?? Date()
+//            // Group and Identify all FPUs together
+//            let fpuID = entries.last?.fpuID ?? ""
+//            // Create an array of all future carb equivalents.
+//            var futureCarbArray = [CarbsEntry]()
+//            while carbEquivalents > 0, numberOfEquivalents > 0 {
+//                if firstIndex {
+//                    useDate = useDate.addingTimeInterval(delay.minutes.timeInterval)
+//                    firstIndex = false
+//                } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
+//
+//                let eachCarbEntry = CarbsEntry(
+//                    id: UUID().uuidString, createdAt: entries.last?.createdAt ?? Date(), actualDate: useDate,
+//                    carbs: equivalent, fat: 0, protein: 0, note: nil,
+//                    enteredBy: CarbsEntry.manual, isFPU: true,
+//                    fpuID: fpuID
+//                )
+//                futureCarbArray.append(eachCarbEntry)
+//                numberOfEquivalents -= 1
+//            }
+//            // Save the array
+//            if carbEquivalents > 0 {
+//                storage.transaction { storage in
+//                    storage.append(futureCarbArray, to: file, uniqBy: \.id)
+//                    uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
+//                        .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
+//                        .sorted { $0.createdAt > $1.createdAt } ?? []
+//                    storage.save(Array(uniqEvents), as: file)
+//                }
+//
+//                // MARK: - save also to core data
+//
+//                saveFPUToCoreDataAsBatchInsert(entries: futureCarbArray)
+//            }
+//        }
+//    }
+//
+//    private func storeNormalCarbs(entries: [CarbsEntry]) {
+//        let file = OpenAPS.Monitor.carbHistory
+//        var uniqEvents: [CarbsEntry] = []
+//
+//        if let entry = entries.last, entry.carbs > 0 {
+//            // uniqEvents = []
+//            let onlyCarbs = CarbsEntry(
+//                id: entry.id ?? "",
+//                createdAt: entry.createdAt,
+//                actualDate: entry.actualDate ?? entry.createdAt,
+//                carbs: entry.carbs,
+//                fat: entry.fat,
+//                protein: entry.protein,
+//                note: entry.note ?? "",
+//                enteredBy: entry.enteredBy ?? "",
+//                isFPU: false,
+//                fpuID: ""
+//            )
+//
+//            storage.transaction { storage in
+//                storage.append(onlyCarbs, to: file, uniqBy: \.id)
+//                uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
+//                    .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
+//                    .sorted { $0.createdAt > $1.createdAt } ?? []
+//                storage.save(Array(uniqEvents), as: file)
+//            }
+//        }
+//    }
+
     private func saveCarbsToCoreData(entries: [CarbsEntry]) {
         guard let entry = entries.last, entry.carbs != 0 else { return }
 
@@ -150,8 +257,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             let newItem = CarbEntryStored(context: self.coredataContext)
             newItem.date = entry.actualDate ?? entry.createdAt
             newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
+            newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
+            newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
             newItem.id = UUID()
             newItem.isFPU = false
+            newItem.isUploadedToNS = false
+
             do {
                 guard self.coredataContext.hasChanges else { return }
                 try self.coredataContext.save()
@@ -162,16 +273,21 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry]) {
-        let commonFPUID = UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the id
+        let commonFPUID =
+            UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
         var entrySlice = ArraySlice(entries) // convert to ArraySlice
         let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
-            guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst() else {
+            guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
+                  let entryId = entry.id
+            else {
                 return true // return true to stop
             }
             carbEntry.date = entry.actualDate
             carbEntry.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
-            carbEntry.id = commonFPUID
+            carbEntry.id = UUID.init(uuidString: entryId)
+            carbEntry.fpuID = commonFPUID
             carbEntry.isFPU = true
+            carbEntry.isUploadedToNS = false
             return false // return false to continue
         }
         coredataContext.perform {
@@ -184,12 +300,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    private func notifyObservers(entries: [CarbsEntry]) {
-        broadcaster.notify(CarbsObserver.self, on: processQueue) {
-            $0.carbsDidUpdate(entries)
-        }
-    }
-
     func syncDate() -> Date {
         Date().addingTimeInterval(-1.days.timeInterval)
     }
@@ -228,32 +338,71 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCarbs, as: [NigtscoutTreatment].self) ?? []
-
-        let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
-        let treatments = eventsManual.map {
-            NigtscoutTreatment(
-                duration: nil,
-                rawDuration: nil,
-                rawRate: nil,
-                absolute: nil,
-                rate: nil,
-                eventType: .nsCarbCorrection,
-                createdAt: $0.actualDate ?? $0.createdAt,
-                enteredBy: CarbsEntry.manual,
-                bolus: nil,
-                insulin: nil,
-                carbs: $0.carbs,
-                fat: nil,
-                protein: nil,
-                foodType: $0.note,
-                targetTop: nil,
-                targetBottom: nil,
-                id: $0.id,
-                fpuID: $0.fpuID
-            )
+    func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToNightscout,
+            key: "date",
+            ascending: false
+        )
+
+        return await coredataContext.perform {
+            return results.map { result in
+                NightscoutTreatment(
+                    duration: nil,
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsCarbCorrection,
+                    createdAt: result.date,
+                    enteredBy: CarbsEntry.manual,
+                    bolus: nil,
+                    insulin: nil,
+                    carbs: Decimal(result.carbs),
+                    fat: Decimal(result.fat),
+                    protein: Decimal(result.protein),
+                    foodType: result.note,
+                    targetTop: nil,
+                    targetBottom: nil,
+                    id: result.id?.uuidString
+                )
+            }
+        }
+    }
+
+    func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.fpusNotYetUploadedToNightscout,
+            key: "date",
+            ascending: false
+        )
+
+        return await coredataContext.perform {
+            return results.map { result in
+                NightscoutTreatment(
+                    duration: nil,
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsCarbCorrection,
+                    createdAt: result.date,
+                    enteredBy: CarbsEntry.manual,
+                    bolus: nil,
+                    insulin: nil,
+                    carbs: Decimal(result.carbs),
+                    fat: Decimal(result.fat),
+                    protein: Decimal(result.protein),
+                    foodType: result.note,
+                    targetTop: nil,
+                    targetBottom: nil,
+                    id: result.fpuID?.uuidString
+                )
+            }
         }
-        return Array(Set(treatments).subtracting(Set(uploaded)))
     }
 }

+ 67 - 89
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -11,11 +11,10 @@ protocol GlucoseStorage {
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
     func isGlucoseFresh() -> Bool
-    func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
-    func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
-    func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment]
+    func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
+    func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     var alarm: GlucoseAlarm? { get }
-    func fetchGlucose() -> [GlucoseStored]
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -81,6 +80,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.glucose = Int16(entry.glucose ?? 0)
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.direction = entry.direction?.symbol
+                        glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
                         debugPrint("\(DebuggingIdentifiers.failed)")
                         debugPrint("\(String(describing: glucoseEntry.direction))")
                         return false // Continue processing
@@ -105,7 +105,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 debug(.deviceManager, "start storage cgmState")
                 self.storage.transaction { storage in
                     let file = OpenAPS.Monitor.cgmState
-                    var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? []
+                    var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
                     var updated = false
                     for x in glucose {
                         debug(.deviceManager, "storeGlucose \(x)")
@@ -127,7 +127,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         if let a = x.activationDate {
                             notes = "\(notes) activated on \(a)"
                         }
-                        let treatment = NigtscoutTreatment(
+                        let treatment = NightscoutTreatment(
                             duration: nil,
                             rawDuration: nil,
                             rawRate: nil,
@@ -135,7 +135,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                             rate: nil,
                             eventType: .nsSensorChange,
                             createdAt: sessionStartDate,
-                            enteredBy: NigtscoutTreatment.local,
+                            enteredBy: NightscoutTreatment.local,
                             bolus: nil,
                             insulin: nil,
                             notes: notes,
@@ -220,118 +220,96 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return filtered
     }
 
-    // MARK: - fetching non manual Glucose, manual Glucose and the last glucose value
-
-    // TODO: -optimize this bullshit here...I would love to use the async/await pattern, but its simply not possible because you would need to change all the calls of the following functions and make them async...same shit with the NSAsynchronousFetchRequest
-    /// its all done on a background thread and on a separate queue so hopefully its not too heavy
-    /// also tried this but here again you need to make everything asynchronous...
-    ///  let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
-    /// privateContext.parent = coredataContext /// merges changes to the core data context
-    ///
-    func fetchGlucose() -> [GlucoseStored] {
-        let predicate = NSPredicate.predicateForOneDayAgo
+    func fetchLatestGlucose() -> GlucoseStored? {
+        let predicate = NSPredicate.predicateFor20MinAgo
         return CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: predicate,
             key: "date",
             ascending: false,
-            fetchLimit: 288,
-            batchSize: 50
-        )
+            fetchLimit: 1
+        ).first
     }
 
-    func fetchManualGlucose() -> [GlucoseStored] {
-        let predicate = NSPredicate.manualGlucose
-        return CoreDataStack.shared.fetchEntities(
+    // Fetch glucose that is not uploaded to Nightscout yet
+    /// Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
+    func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
-            predicate: predicate,
+            predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             key: "date",
             ascending: false,
-            fetchLimit: 288,
-            batchSize: 50
+            fetchLimit: 288
         )
-    }
-
-    func fetchLatestGlucose() -> GlucoseStored? {
-        let predicate = NSPredicate.predicateFor20MinAgo
-        return CoreDataStack.shared.fetchEntities(
-            ofType: GlucoseStored.self,
-            onContext: coredataContext,
-            predicate: predicate,
-            key: "date",
-            ascending: false,
-            fetchLimit: 1
-        ).first
-    }
-
-    private func processManualGlucose() -> [BloodGlucose] {
-        coredataContext.performAndWait {
-            let fetchedResults = fetchManualGlucose()
-            let glucoseArray = fetchedResults.map { result in
+        return await coredataContext.perform {
+            return results.map { result in
                 BloodGlucose(
+                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    sgv: Int(result.glucose),
+                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
                     dateString: result.date ?? Date(),
                     unfiltered: Decimal(result.glucose),
                     filtered: Decimal(result.glucose),
                     noise: nil,
-                    type: ""
+                    glucose: Int(result.glucose)
                 )
             }
-            return glucoseArray
         }
     }
 
-    private func processGlucose() -> [BloodGlucose] {
-        coredataContext.performAndWait {
-            let results = self.fetchGlucose()
-            let glucoseArray = results.map { result in
-                BloodGlucose(
-                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
-                    dateString: result.date ?? Date(),
-                    unfiltered: Decimal(result.glucose),
-                    filtered: Decimal(result.glucose),
-                    noise: nil,
-                    type: ""
+    // Fetch manual glucose that is not uploaded to Nightscout yet
+    /// Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
+    func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+        return await coredataContext.perform {
+            return results.map { result in
+                NightscoutTreatment(
+                    duration: nil,
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .capillaryGlucose,
+                    createdAt: result.date,
+                    enteredBy: "Trio",
+                    bolus: nil,
+                    insulin: nil,
+                    notes: "Trio User",
+                    carbs: nil,
+                    fat: nil,
+                    protein: nil,
+                    foodType: nil,
+                    targetTop: nil,
+                    targetBottom: nil,
+                    glucoseType: "Manual",
+                    glucose: self.settingsManager.settings
+                        .units == .mgdL ? (self.glucoseFormatter.string(from: Int(result.glucose) as NSNumber) ?? "")
+                        : (self.glucoseFormatter.string(from: Decimal(result.glucose).asMmolL as NSNumber) ?? ""),
+                    units: self.settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl",
+                    id: result.id?.uuidString
                 )
             }
-            return glucoseArray
         }
     }
 
-    func nightscoutGlucoseNotUploaded() -> [BloodGlucose] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? []
-        let recentGlucose = processGlucose()
+    func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        async let alreadyUploaded: [NightscoutTreatment] = storage
+            .retrieveAsync(OpenAPS.Nightscout.uploadedCGMState, as: [NightscoutTreatment].self) ?? []
+        async let allValues: [NightscoutTreatment] = storage
+            .retrieveAsync(OpenAPS.Monitor.cgmState, as: [NightscoutTreatment].self) ?? []
 
-        return Array(Set(recentGlucose).subtracting(Set(uploaded)))
-    }
-
-    func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
-        let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
-        return Array(Set(recent).subtracting(Set(uploaded)))
-    }
-
-    func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = (storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? [])
-            .filter({ $0.type == GlucoseType.manual.rawValue })
-
-        let recent = processManualGlucose()
-        let filtered = Array(Set(recent).subtracting(Set(uploaded)))
-        let manualReadings = filtered.map { item -> NigtscoutTreatment in
-            NigtscoutTreatment(
-                duration: nil, rawDuration: nil, rawRate: nil, absolute: nil, rate: nil, eventType: .capillaryGlucose,
-                createdAt: item.dateString, enteredBy: "iAPS", bolus: nil, insulin: nil, notes: "iAPS User", carbs: nil,
-                fat: nil,
-                protein: nil, foodType: nil, targetTop: nil, targetBottom: nil, glucoseType: "Manual",
-                glucose: settingsManager.settings
-                    .units == .mgdL ? (glucoseFormatter.string(from: Int(item.glucose ?? 100) as NSNumber) ?? "")
-                    : (glucoseFormatter.string(from: Decimal(item.glucose ?? 100).asMmolL as NSNumber) ?? ""),
-                units: settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl"
-            )
-        }
-        return manualReadings
+        let (alreadyUploadedValues, allValuesSet) = await (alreadyUploaded, allValues)
+        return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
     }
 
     var alarm: GlucoseAlarm? {

+ 180 - 0
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -0,0 +1,180 @@
+import CoreData
+import Foundation
+import Swinject
+
+protocol OverrideStorage {
+    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
+    func fetchForOverridePresets() async -> [NSManagedObjectID]
+    func calculateTarget(override: OverrideStored) -> Decimal
+    func storeOverride(override: Override) async
+    func copyRunningOverride(_ override: OverrideStored) async
+    func deleteOverridePreset(_ objectID: NSManagedObjectID) async
+}
+
+final class BaseOverrideStorage: OverrideStorage, Injectable {
+    @Injected() private var settingsManager: SettingsManager!
+
+    private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    private var dateFormatter: DateFormatter {
+        let df = DateFormatter()
+        df.dateFormat = "dd.MM.yy HH:mm"
+        return df
+    }
+
+    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.lastActiveOverride,
+            key: "orderPosition",
+            ascending: true,
+            fetchLimit: fetchLimit
+        )
+
+        return await backgroundContext.perform {
+            return results.map(\.objectID)
+        }
+    }
+
+    /// Returns the NSManagedObjectID of the Override Presets
+    func fetchForOverridePresets() async -> [NSManagedObjectID] {
+        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.allOverridePresets,
+            key: "orderPosition",
+            ascending: true
+        )
+
+        return await backgroundContext.perform {
+            return result.map(\.objectID)
+        }
+    }
+
+    @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
+        guard let overrideTarget = override.target, overrideTarget != 0 else {
+            return 100 // default
+        }
+        return overrideTarget.decimalValue
+    }
+
+    func storeOverride(override: Override) async {
+        var presetCount = -1
+        if override.isPreset {
+            let presets = await fetchForOverridePresets()
+            presetCount = presets.count
+        }
+
+        await backgroundContext.perform {
+            let newOverride = OverrideStored(context: self.backgroundContext)
+
+            // override key meta data
+            if !override.name.isEmpty {
+                newOverride.name = override.name
+            } else {
+                let formattedDate = self.dateFormatter.string(from: Date())
+                newOverride.name = "Preset <\(formattedDate)>"
+            }
+            newOverride.id = UUID().uuidString
+            newOverride.date = override.date
+            newOverride.isPreset = override.isPreset
+
+            // Assign orderPosition if it's a preset and presetCount is valid
+            if override.isPreset, presetCount > -1 {
+                newOverride.orderPosition = Int16(presetCount + 1) // Ensure type matches Core Data model
+            }
+
+            // override metrics
+            newOverride.duration = override.duration as NSDecimalNumber
+            newOverride.indefinite = override.indefinite
+            newOverride.percentage = override.percentage
+            newOverride.enabled = override.enabled
+            newOverride.smbIsOff = override.smbIsOff
+            if override.overrideTarget {
+                newOverride.target = (
+                    self.settingsManager.settings.units == .mmolL ? override.target.asMgdL : override.target
+                ) as NSDecimalNumber
+            } else {
+                newOverride.target = 0
+            }
+            if override.advancedSettings {
+                newOverride.advancedSettings = true
+
+                if !override.isfAndCr {
+                    newOverride.isfAndCr = false
+                    newOverride.isf = override.isf
+                    newOverride.cr = override.cr
+                } else {
+                    newOverride.isfAndCr = true
+                }
+
+                if override.smbIsAlwaysOff {
+                    newOverride.smbIsAlwaysOff = true
+                    newOverride.start = override.start as NSDecimalNumber
+                    newOverride.end = override.end as NSDecimalNumber
+                } else {
+                    newOverride.smbIsAlwaysOff = false
+                }
+
+                newOverride.smbMinutes = override.smbMinutes as NSDecimalNumber
+                newOverride.uamMinutes = override.uamMinutes as NSDecimalNumber
+            }
+
+            do {
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Preset to Core Data with error: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    // Copy the current Override if it is a RUNNING Preset
+    /// otherwise we would edit the Preset
+    @MainActor func copyRunningOverride(_ override: OverrideStored) async {
+        let newOverride = OverrideStored(context: viewContext)
+        newOverride.duration = override.duration
+        newOverride.indefinite = override.indefinite
+        newOverride.percentage = override.percentage
+        newOverride.smbIsOff = override.smbIsOff
+        newOverride.name = override.name
+        newOverride.isPreset = false // no Preset
+        newOverride.date = Date()
+        newOverride.enabled = override.enabled
+        newOverride.target = override.target
+        newOverride.advancedSettings = override.advancedSettings
+        newOverride.isfAndCr = override.isfAndCr
+        newOverride.isf = override.isf
+        newOverride.cr = override.cr
+        newOverride.smbIsAlwaysOff = override.smbIsAlwaysOff
+        newOverride.start = override.start
+        newOverride.end = override.end
+        newOverride.smbMinutes = override.smbMinutes
+        newOverride.uamMinutes = override.uamMinutes
+
+        await viewContext.perform {
+            do {
+                guard self.viewContext.hasChanges else { return }
+                try self.viewContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to copy Override with error: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    /// marked as MainActor to be able to publish changes from the background
+    /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+    @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
+        await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
+    }
+}

+ 200 - 221
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -10,11 +10,9 @@ protocol PumpHistoryObserver {
 
 protocol PumpHistoryStorage {
     func storePumpEvents(_ events: [NewPumpEvent])
-    func storeEvents(_ events: [PumpHistoryEvent])
-    func storeJournalCarbs(_ carbs: Int)
+    func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func recent() -> [PumpHistoryEvent]
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
-    func saveCancelTempEvents()
+    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteInsulin(at date: Date)
 }
 
@@ -82,12 +80,14 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         }
 
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.bolus.rawValue
+                        newPumpEvent.isUploadedToNS = false
 
                         let newBolusEntry = BolusStored(context: self.context)
                         newBolusEntry.pumpEvent = newPumpEvent
-                        newBolusEntry.amount = amount as? NSDecimalNumber
+                        newBolusEntry.amount = NSDecimalNumber(decimal: amount)
                         newBolusEntry.isExternal = dose.manuallyEntered
                         newBolusEntry.isSMB = dose.automatic ?? true
 
@@ -109,8 +109,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         guard !isCancel else { continue }
 
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = date
                         newPumpEvent.type = PumpEvent.tempBasal.rawValue
+                        newPumpEvent.isUploadedToNS = false
 
                         let newTempBasal = TempBasalStored(context: self.context)
                         newTempBasal.pumpEvent = newPumpEvent
@@ -125,8 +127,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             continue
                         }
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
+                        newPumpEvent.isUploadedToNS = false
 
                     case .resume:
                         guard existingEvents.isEmpty else {
@@ -135,8 +139,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             continue
                         }
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpResume.rawValue
+                        newPumpEvent.isUploadedToNS = false
 
                     case .rewind:
                         guard existingEvents.isEmpty else {
@@ -145,8 +151,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             continue
                         }
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.rewind.rawValue
+                        newPumpEvent.isUploadedToNS = false
 
                     case .prime:
                         guard existingEvents.isEmpty else {
@@ -155,8 +163,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             continue
                         }
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.prime.rawValue
+                        newPumpEvent.isUploadedToNS = false
 
                     case .alarm:
                         guard existingEvents.isEmpty else {
@@ -165,8 +175,11 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             continue
                         }
                         let newPumpEvent = PumpEventStored(context: self.context)
+                        newPumpEvent.id = UUID().uuidString
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
+                        newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.note = event.title
 
                     default:
                         continue
@@ -184,38 +197,28 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
-    func storeJournalCarbs(_ carbs: Int) {
-        processQueue.async {
-            let eventsToStore = [
-                PumpHistoryEvent(
-                    id: UUID().uuidString,
-                    type: .journalCarbs,
-                    timestamp: Date(),
-                    amount: nil,
-                    duration: nil,
-                    durationMin: nil,
-                    rate: nil,
-                    temp: nil,
-                    carbInput: carbs
-                )
-            ]
-            self.storeEvents(eventsToStore)
-        }
-    }
-
-    func storeEvents(_ events: [PumpHistoryEvent]) {
-        processQueue.async {
-            let file = OpenAPS.Monitor.pumpHistory
-            var uniqEvents: [PumpHistoryEvent] = []
-            self.storage.transaction { storage in
-                storage.append(events, to: file, uniqBy: \.id)
-                uniqEvents = storage.retrieve(file, as: [PumpHistoryEvent].self)?
-                    .filter { $0.timestamp.addingTimeInterval(1.days.timeInterval) > Date() }
-                    .sorted { $0.timestamp > $1.timestamp } ?? []
-                storage.save(Array(uniqEvents), as: file)
-            }
-            self.broadcaster.notify(PumpHistoryObserver.self, on: self.processQueue) {
-                $0.pumpHistoryDidUpdate(uniqEvents)
+    func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async {
+        debug(.default, "External insulin saved")
+        await context.perform {
+            // create pump event
+            let newPumpEvent = PumpEventStored(context: self.context)
+            newPumpEvent.id = UUID().uuidString
+            newPumpEvent.timestamp = timestamp
+            newPumpEvent.type = PumpEvent.bolus.rawValue
+            newPumpEvent.isUploadedToNS = false
+
+            // create bolus entry and specify relationship to pump event
+            let newBolusEntry = BolusStored(context: self.context)
+            newBolusEntry.pumpEvent = newPumpEvent
+            newBolusEntry.amount = amount as NSDecimalNumber
+            newBolusEntry.isExternal = true // we are creating an external dose
+            newBolusEntry.isSMB = false // the dose is manually administered
+
+            do {
+                guard self.context.hasChanges else { return }
+                try self.context.save()
+            } catch {
+                print(error.localizedDescription)
             }
         }
     }
@@ -238,197 +241,173 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
-    func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
-        if event.isSMB ?? false {
+    func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
+        if event.bolus!.isSMB {
             return .smb
         }
-        if event.isExternal ?? false {
+        if event.bolus!.isExternal {
             return .isExternal
         }
-        return event.type
+        return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
     }
 
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let events = recent()
-        guard !events.isEmpty else { return [] }
-
-        let temps: [NigtscoutTreatment] = events.reduce([]) { result, event in
-            var result = result
-            switch event.type {
-            case .tempBasal:
-                result.append(NigtscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: event,
-                    absolute: event.rate,
-                    rate: event.rate,
-                    eventType: .nsTempBasal,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                ))
-            case .tempBasalDuration:
-                if var last = result.popLast(), last.eventType == .nsTempBasal, last.createdAt == event.timestamp {
-                    last.duration = event.durationMin
-                    last.rawDuration = event
-                    result.append(last)
-                }
-            default: break
-            }
-            return result
-        }
-
-        let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in
-            switch event.type {
-            case .bolus:
-                let eventType = determineBolusEventType(for: event)
-                return NigtscoutTreatment(
-                    duration: event.duration,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: eventType,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: event,
-                    insulin: event.amount,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            case .journalCarbs:
-                return NigtscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsCarbCorrection,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: Decimal(event.carbInput ?? 0),
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            default: return nil
-            }
-        }
+    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        let fetchedPumpEvents = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        return await context.perform { [self] in
+            fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEvent.bolus.rawValue:
+                    // eventType determines whether bolus is external, smb or manual (=administered via app by user)
+                    let eventType = determineBolusEventType(for: event)
+                    return NightscoutTreatment(
+                        duration: nil,
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: nil,
+                        rate: nil,
+                        eventType: eventType,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: event.bolus?.amount as Decimal?,
+                        notes: nil,
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil,
+                        id: event.id
+                    )
+                case PumpEvent.tempBasal.rawValue:
+                    return NightscoutTreatment(
+                        duration: Int(event.tempBasal?.duration ?? 0),
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: event.tempBasal?.rate as Decimal?,
+                        rate: event.tempBasal?.rate as Decimal?,
+                        eventType: .nsTempBasal,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: nil,
+                        notes: nil,
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil,
+                        id: event.id
+                    )
+                // TODO: should we really upload pumpSuspend as announcement ?!
+                case PumpEvent.pumpSuspend.rawValue:
+                    return NightscoutTreatment(
+                        duration: nil,
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: nil,
+                        rate: nil,
+                        eventType: .nsAnnouncement,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: nil,
+                        notes: PumpEvent.pumpSuspend.rawValue,
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil
+                    )
+                // TODO: should we really upload pumpResume as announcement ?!
+                case PumpEvent.pumpResume.rawValue:
+                    return NightscoutTreatment(
+                        duration: nil,
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: nil,
+                        rate: nil,
+                        eventType: .nsAnnouncement,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: nil,
+                        notes: PumpEvent.pumpResume.rawValue,
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil
+                    )
+                case PumpEvent.rewind.rawValue:
+                    return NightscoutTreatment(
+                        duration: nil,
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: nil,
+                        rate: nil,
+                        eventType: .nsInsulinChange,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: nil,
+                        notes: nil,
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil
+                    )
+                case PumpEvent.prime.rawValue:
+                    return NightscoutTreatment(
+                        duration: nil,
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: nil,
+                        rate: nil,
+                        eventType: .nsSiteChange,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: nil,
+                        notes: nil,
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil
+                    )
+                case PumpEvent.pumpAlarm.rawValue:
+                    return NightscoutTreatment(
+                        duration: 30, // minutes
+                        rawDuration: nil,
+                        rawRate: nil,
+                        absolute: nil,
+                        rate: nil,
+                        eventType: .nsAnnouncement,
+                        createdAt: event.timestamp,
+                        enteredBy: NightscoutTreatment.local,
+                        bolus: nil,
+                        insulin: nil,
+                        notes: "Alarm \(String(describing: event.note)) \(PumpEvent.pumpAlarm.rawValue)",
+                        carbs: nil,
+                        fat: nil,
+                        protein: nil,
+                        targetTop: nil,
+                        targetBottom: nil
+                    )
 
-        let misc = events.compactMap { event -> NigtscoutTreatment? in
-            switch event.type {
-            case .prime:
-                return NigtscoutTreatment(
-                    duration: event.duration,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsSiteChange,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: event,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            case .rewind:
-                return NigtscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsInsulinChange,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: nil,
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            case .pumpAlarm:
-                return NigtscoutTreatment(
-                    duration: 30, // minutes
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .nsAnnouncement,
-                    createdAt: event.timestamp,
-                    enteredBy: NigtscoutTreatment.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: "Alarm \(String(describing: event.note)) \(event.type)",
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    targetTop: nil,
-                    targetBottom: nil
-                )
-            default: return nil
-            }
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
         }
-
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
-
-        let treatments = Array(Set([bolusesAndCarbs, temps, misc].flatMap { $0 }).subtracting(Set(uploaded)))
-
-        return treatments.sorted { $0.createdAt! > $1.createdAt! }
-    }
-
-    func saveCancelTempEvents() {
-        let basalID = UUID().uuidString
-        let date = Date()
-
-        let events = [
-            PumpHistoryEvent(
-                id: basalID,
-                type: .tempBasalDuration,
-                timestamp: date,
-                amount: nil,
-                duration: nil,
-                durationMin: 0,
-                rate: nil,
-                temp: nil,
-                carbInput: nil
-            ),
-            PumpHistoryEvent(
-                id: "_" + basalID,
-                type: .tempBasal,
-                timestamp: date,
-                amount: nil,
-                duration: nil,
-                durationMin: nil,
-                rate: 0,
-                temp: .absolute,
-                carbInput: nil
-            )
-        ]
-
-        storeEvents(events)
     }
 }

+ 4 - 4
FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift

@@ -10,7 +10,7 @@ protocol TempTargetsStorage {
     func storeTempTargets(_ targets: [TempTarget])
     func syncDate() -> Date
     func recent() -> [TempTarget]
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
+    func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment]
     func storePresets(_ targets: [TempTarget])
     func presets() -> [TempTarget]
     func current() -> TempTarget?
@@ -82,12 +82,12 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return last
     }
 
-    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedTempTargets, as: [NigtscoutTreatment].self) ?? []
+    func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment] {
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedTempTargets, as: [NightscoutTreatment].self) ?? []
 
         let eventsManual = recent().filter { $0.enteredBy == TempTarget.manual }
         let treatments = eventsManual.map {
-            NigtscoutTreatment(
+            NightscoutTreatment(
                 duration: Int($0.duration),
                 rawDuration: nil,
                 rawRate: nil,

+ 2 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -109,6 +109,8 @@ import Swinject
         try await coreDataStack.batchDeleteOlderThan(OpenAPS_Battery.self, dateKey: "date", days: 90)
         try await coreDataStack.batchDeleteOlderThan(CarbEntryStored.self, dateKey: "date", days: 90)
         try await coreDataStack.batchDeleteOlderThan(Forecast.self, dateKey: "date", days: 90)
+        try await coreDataStack.batchDeleteOlderThan(OverrideStored.self, dateKey: "date", days: 3)
+        try await coreDataStack.batchDeleteOlderThan(OverrideRunStored.self, dateKey: "startDate", days: 3)
 
         // TODO: - Purge Data of other (future) entities as well
     }

+ 1 - 0
FreeAPS/Sources/Assemblies/StorageAssembly.swift

@@ -8,6 +8,7 @@ final class StorageAssembly: Assembly {
         }
         container.register(FileStorage.self) { _ in BaseFileStorage() }
         container.register(PumpHistoryStorage.self) { r in BasePumpHistoryStorage(resolver: r) }
+        container.register(OverrideStorage.self) { r in BaseOverrideStorage(resolver: r) }
         container.register(GlucoseStorage.self) { r in BaseGlucoseStorage(resolver: r) }
         container.register(TempTargetsStorage.self) { r in BaseTempTargetsStorage(resolver: r) }
         container.register(CarbsStorage.self) { r in BaseCarbsStorage(resolver: r) }

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

@@ -15,7 +15,7 @@ struct AlertEntry: JSON, Codable, Hashable {
     let contentBody: String?
     var errorMessage: String?
 
-    static let manual = "iAPS"
+    static let manual = "Trio"
 
     static func == (lhs: AlertEntry, rhs: AlertEntry) -> Bool {
         lhs.issuedDate == rhs.issuedDate

+ 3 - 3
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -16,9 +16,9 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
         case rateOutOfRange = "RATE OUT OF RANGE"
     }
 
-    var _id = UUID().uuidString
+    var _id: String?
     var id: String {
-        _id
+        _id ?? UUID().uuidString
     }
 
     var sgv: Int?
@@ -29,7 +29,7 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     let filtered: Decimal?
     let noise: Int?
     var glucose: Int?
-    let type: String?
+    var type: String? = nil
     var activationDate: Date? = nil
     var sessionStartDate: Date? = nil
     var transmitterID: String? = nil

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

@@ -12,7 +12,7 @@ struct CarbsEntry: JSON, Equatable, Hashable, Identifiable {
     let isFPU: Bool?
     let fpuID: String?
 
-    static let manual = "iAPS"
+    static let manual = "Trio"
     static let appleHealth = "applehealth"
 
     static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool {

+ 6 - 6
FreeAPS/Sources/Models/NightscoutTreatment.swift

@@ -1,12 +1,12 @@
 import Foundation
 
-struct NigtscoutTreatment: JSON, Hashable, Equatable {
+struct NightscoutTreatment: JSON, Hashable, Equatable {
     var duration: Int?
     var rawDuration: PumpHistoryEvent?
     var rawRate: PumpHistoryEvent?
     var absolute: Decimal?
     var rate: Decimal?
-    var eventType: EventType
+    var eventType: PumpEventStored.EventType
     var createdAt: Date?
     var enteredBy: String?
     var bolus: PumpHistoryEvent?
@@ -24,11 +24,11 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     var id: String?
     var fpuID: String?
 
-    static let local = "iAPS"
+    static let local = "Trio"
 
-    static let empty = NigtscoutTreatment(from: "{}")!
+    static let empty = NightscoutTreatment(from: "{}")!
 
-    static func == (lhs: NigtscoutTreatment, rhs: NigtscoutTreatment) -> Bool {
+    static func == (lhs: NightscoutTreatment, rhs: NightscoutTreatment) -> Bool {
         (lhs.createdAt ?? Date()) == (rhs.createdAt ?? Date())
     }
 
@@ -37,7 +37,7 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     }
 }
 
-extension NigtscoutTreatment {
+extension NightscoutTreatment {
     private enum CodingKeys: String, CodingKey {
         case duration
         case rawDuration = "raw_duration"

+ 24 - 0
FreeAPS/Sources/Models/Override.swift

@@ -0,0 +1,24 @@
+import Foundation
+
+struct Override {
+    let name: String
+    let enabled: Bool
+    let date: Date
+    let duration: Decimal
+    let indefinite: Bool
+    let percentage: Double
+    let smbIsOff: Bool
+    let isPreset: Bool
+    let id: String
+    let overrideTarget: Bool
+    let target: Decimal
+    let advancedSettings: Bool
+    let isfAndCr: Bool
+    let isf: Bool
+    let cr: Bool
+    let smbIsAlwaysOff: Bool
+    let start: Decimal
+    let end: Decimal
+    let smbMinutes: Decimal
+    let uamMinutes: Decimal
+}

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

@@ -10,7 +10,7 @@ struct TempTarget: JSON, Identifiable, Equatable, Hashable {
     let enteredBy: String?
     let reason: String?
 
-    static let manual = "iAPS"
+    static let manual = "Trio"
     static let custom = "Temp target"
     static let cancel = "Cancel"
 

+ 11 - 53
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -80,7 +80,7 @@ extension Bolus {
         @Published var carbsRequired: Decimal?
         @Published var useFPUconversion: Bool = false
         @Published var dish: String = ""
-        @Published var selection: Presets?
+        @Published var selection: MealPresetStored?
         @Published var summation: [String] = []
         @Published var maxCarbs: Decimal = 0
 
@@ -234,10 +234,12 @@ extension Bolus {
             Task {
                 let isInsulinGiven = amount > 0
                 let isCarbsPresent = carbs > 0
+                let isFatPresent = fat > 0
+                let isProteinPresent = protein > 0
 
                 if isInsulinGiven {
                     try await handleInsulin(isExternal: externalInsulin)
-                } else if isCarbsPresent {
+                } else if isCarbsPresent || isFatPresent || isProteinPresent {
                     waitForSuggestion = true
                 } else {
                     hideModal()
@@ -327,7 +329,10 @@ extension Bolus {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
-                    storeExternalInsulinEvent()
+                    // store external dose to pump history
+                    await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
+                    // perform determine basal sync
+                    await apsManager.determineBasalSync()
                 } else {
                     print("authentication failed")
                 }
@@ -342,53 +347,6 @@ extension Bolus {
             }
         }
 
-        private func storeExternalInsulinEvent() {
-            pumpHistoryStorage.storeEvents(
-                [
-                    PumpHistoryEvent(
-                        id: UUID().uuidString,
-                        type: .bolus,
-                        timestamp: date,
-                        amount: amount,
-                        duration: nil,
-                        durationMin: nil,
-                        rate: nil,
-                        temp: nil,
-                        carbInput: nil,
-                        isExternal: true
-                    )
-                ]
-            )
-            debug(.default, "External insulin saved")
-
-            // save to core data asynchronously
-            context.perform {
-                // create pump event
-                let newPumpEvent = PumpEventStored(context: self.context)
-                newPumpEvent.timestamp = self.date
-                newPumpEvent.type = PumpEvent.bolus.rawValue
-
-                // create bolus entry and specify relationship to pump event
-                let newBolusEntry = BolusStored(context: self.context)
-                newBolusEntry.pumpEvent = newPumpEvent
-                newBolusEntry.amount = self.amount as NSDecimalNumber
-                newBolusEntry.isExternal = true
-                newBolusEntry.isSMB = false
-
-                do {
-                    guard self.context.hasChanges else { return }
-                    try self.context.save()
-                } catch {
-                    print(error.localizedDescription)
-                }
-            }
-
-            // perform determine basal sync
-            Task {
-                await apsManager.determineBasalSync()
-            }
-        }
-
         // MARK: - Carbs
 
         func saveMeal() {
@@ -409,7 +367,7 @@ extension Bolus {
             )]
             carbsStorage.storeCarbs(carbsToStore)
 
-            if carbs > 0 {
+            if carbs > 0 || fat > 0 || protein > 0 {
                 // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
                 if amount <= 0 {
                     Task {
@@ -473,10 +431,10 @@ extension Bolus {
             var carbs_: Decimal = 0.0
             var fat_: Decimal = 0.0
             var protein_: Decimal = 0.0
-            var presetArray = [Presets]()
+            var presetArray = [MealPresetStored]()
 
             context.performAndWait {
-                let requestPresets = Presets.fetchRequest() as NSFetchRequest<Presets>
+                let requestPresets = MealPresetStored.fetchRequest() as NSFetchRequest<MealPresetStored>
                 try? presetArray = context.fetch(requestPresets)
             }
             var waitersNotepad = [String]()

+ 6 - 6
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -37,9 +37,9 @@ extension Bolus {
         @Environment(\.colorScheme) var colorScheme
 
         @FetchRequest(
-            entity: Presets.entity(),
+            entity: MealPresetStored.entity(),
             sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
-        ) var carbPresets: FetchedResults<Presets>
+        ) var carbPresets: FetchedResults<MealPresetStored>
 
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -109,7 +109,7 @@ extension Bolus {
                     Button {
                         saved = true
                         if dish != "", saved {
-                            let preset = Presets(context: moc)
+                            let preset = MealPresetStored(context: moc)
                             preset.dish = dish
                             preset.fat = state.fat as NSDecimalNumber
                             preset.protein = state.protein as NSDecimalNumber
@@ -193,9 +193,9 @@ extension Bolus {
                         minusButton
                     }
                     Picker("Preset", selection: $state.selection) {
-                        Text("Saved Food").tag(nil as Presets?)
-                        ForEach(carbPresets, id: \.self) { (preset: Presets) in
-                            Text(preset.dish ?? "").tag(preset as Presets?)
+                        Text("Saved Food").tag(nil as MealPresetStored?)
+                        ForEach(carbPresets, id: \.self) { (preset: MealPresetStored) in
+                            Text(preset.dish ?? "").tag(preset as MealPresetStored?)
                         }
                     }
                     .labelsHidden()

+ 2 - 2
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -218,6 +218,6 @@ enum DataTable {
 }
 
 protocol DataTableProvider: Provider {
-    func deleteCarbs(_ treatment: CarbEntryStored)
-    func deleteInsulin(with treatmentObjectID: NSManagedObjectID)
+    func deleteCarbsFromNightscout(withID id: String) async
+    func deleteInsulin(with treatmentObjectID: NSManagedObjectID) async
 }

+ 26 - 10
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -12,24 +12,32 @@ extension DataTable {
                 ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2)
         }
 
-        func deleteCarbs(_: CarbEntryStored) {
-            // TODO: fix this and refactor nightscoutManager.deleteCarbs()
-//            nightscoutManager.deleteCarbs(treatment, complexMeal: false)
+        func deleteCarbsFromNightscout(withID id: String) {
+            Task {
+                await nightscoutManager.deleteCarbs(withID: id)
+            }
         }
 
-        func deleteInsulin(with treatmentObjectID: NSManagedObjectID) {
+        func deleteInsulin(with treatmentObjectID: NSManagedObjectID) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
 
-            taskContext.perform {
+            await taskContext.perform {
                 do {
                     guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
                     else {
                         debug(.default, "Could not cast the object to PumpEventStored")
                         return
                     }
-                    self.nightscoutManager.deleteInsulin(at: treatmentToDelete.timestamp ?? Date())
-                    let id = treatmentToDelete.id
-                    self.healthkitManager.deleteInsulin(syncID: id)
+
+                    // Delete Insulin from Nightscout
+                    if let id = treatmentToDelete.id {
+                        self.deleteInsulinFromNightscout(withID: id)
+                    }
+
+                    // TODO: - Rewrite healthkit implementation
+
+//                    let id = treatmentToDelete.id
+//                    self.healthkitManager.deleteInsulin(syncID: id)
 
                     taskContext.delete(treatmentToDelete)
                     try taskContext.save()
@@ -41,8 +49,16 @@ extension DataTable {
             }
         }
 
-        func deleteManualGlucose(date: Date?) {
-            nightscoutManager.deleteManualGlucose(at: date ?? .distantPast)
+        func deleteInsulinFromNightscout(withID id: String) {
+            Task {
+                await nightscoutManager.deleteInsulin(withID: id)
+            }
+        }
+
+        func deleteManualGlucose(withID id: String) {
+            Task {
+                await nightscoutManager.deleteManualGlucose(withID: id)
+            }
         }
     }
 }

+ 39 - 31
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -45,17 +45,22 @@ extension DataTable {
             taskContext.name = "deleteContext"
             taskContext.transactionAuthor = "deleteGlucose"
 
-            var glucose: GlucoseStored?
-
             await taskContext.perform {
                 do {
-                    glucose = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
+                    let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
 
-                    guard let glucoseToDelete = glucose else {
+                    guard let glucoseToDelete = result else {
                         debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
                         return
                     }
 
+                    // Delete Manual Glucose from Nightscout
+                    if glucoseToDelete.isManual == true {
+                        if let id = glucoseToDelete.id?.uuidString {
+                            self.provider.deleteManualGlucose(withID: id)
+                        }
+                    }
+
                     taskContext.delete(glucoseToDelete)
 
                     guard taskContext.hasChanges else { return }
@@ -67,15 +72,6 @@ extension DataTable {
                     )
                 }
             }
-
-            guard let glucoseToDelete = glucose else {
-                debugPrint(
-                    "Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found after task context execution"
-                )
-                return
-            }
-
-            provider.deleteManualGlucose(date: glucoseToDelete.date)
         }
 
         // Carb and FPU deletion from history
@@ -104,10 +100,13 @@ extension DataTable {
                         return
                     }
 
-                    if carbEntry.isFPU, let fpuID = carbEntry.id {
+                    if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                        // Delete FPUs from Nightscout
+                        self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
+
                         // fetch request for all carb entries with the same id
                         let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                        fetchRequest.predicate = NSPredicate(format: "id == %@", fpuID as CVarArg)
+                        fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
 
                         // NSBatchDeleteRequest
                         let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
@@ -117,14 +116,19 @@ extension DataTable {
                         let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
                         debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
 
-                        guard taskContext.hasChanges else { return }
-                        try taskContext.save()
-
+                        Foundation.NotificationCenter.default.post(name: .didPerformBatchDelete, object: nil)
                     } else {
+                        // Delete carbs from Nightscout
+                        if let id = carbEntry.id?.uuidString {
+                            self.provider.deleteCarbsFromNightscout(withID: id)
+                        }
+
+                        // Now delete carbs also from the Database
                         taskContext.delete(carbEntry)
 
                         guard taskContext.hasChanges else { return }
                         try taskContext.save()
+
                         debugPrint(
                             "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
                         )
@@ -135,11 +139,8 @@ extension DataTable {
                 }
             }
 
-            // Delete carbs also from Nightscout and perform a determine basal sync to update cob
-            if let carbEntry = carbEntry {
-                provider.deleteCarbs(carbEntry)
-                await apsManager.determineBasalSync()
-            }
+            // Perform a determine basal sync to update cob
+            await apsManager.determineBasalSync()
         }
 
         // Insulin deletion from history
@@ -156,18 +157,24 @@ extension DataTable {
         func deleteInsulin(_ treatmentObjectID: NSManagedObjectID) async {
             do {
                 let authenticated = try await unlockmanager.unlock()
-                if authenticated {
-                    CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
-                    provider.deleteInsulin(with: treatmentObjectID)
+                guard authenticated else {
+                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
+                    return
+                }
 
-                    await apsManager.determineBasalSync()
+                async let deleteNSManagedObjectTask: () = CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
+                async let deleteInsulinFromNightScoutTask: () = provider.deleteInsulin(with: treatmentObjectID)
+                async let determineBasalTask: () = apsManager.determineBasalSync()
+
+                await deleteNSManagedObjectTask
+                await deleteInsulinFromNightScoutTask
+                await determineBasalTask
 
-                } else {
-                    print("authentication failed")
-                }
             } catch {
-                print("authentication error: \(error.localizedDescription)")
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
+                )
             }
         }
 
@@ -182,6 +189,7 @@ extension DataTable {
                 newItem.date = Date()
                 newItem.glucose = Int16(glucoseAsInt)
                 newItem.isManual = true
+                newItem.isUploadedToNS = false
 
                 do {
                     guard self.coredataContext.hasChanges else { return }

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

@@ -76,7 +76,9 @@ extension Home {
         @Published var suspensions: [PumpEventStored] = []
         @Published var batteryFromPersistence: [OpenAPS_Battery] = []
         @Published var lastPumpBolus: PumpEventStored?
-
+        @Published var overrides: [OverrideStored] = []
+        @Published var overrideRunStored: [OverrideRunStored] = []
+        @Published var isOverrideCancelled: Bool = false
         let context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -96,6 +98,8 @@ extension Home {
             setupReservoir()
             setupAnnouncements()
             setupCurrentPumpTimezone()
+            setupOverrides()
+            setupOverrideRunStored()
 
             uploadStats = settingsManager.settings.uploadStats
             units = settingsManager.settings.units
@@ -425,6 +429,14 @@ extension Home.StateModel {
             name: .didPerformBatchInsert,
             object: nil
         )
+
+        /// custom notification that is sent when a batch delete of fpus is done
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleBatchDelete),
+            name: .didPerformBatchDelete,
+            object: nil
+        )
     }
 
     /// determine the actions when the context has changed
@@ -442,6 +454,10 @@ extension Home.StateModel {
         setupGlucoseArray()
     }
 
+    @objc private func handleBatchDelete() {
+        setupFPUsArray()
+    }
+
     private func processUpdates(userInfo: [AnyHashable: Any]) async {
         var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
         objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
@@ -453,6 +469,8 @@ extension Home.StateModel {
         let carbUpdates = objects.filter { $0 is CarbEntryStored }
         let insulinUpdates = objects.filter { $0 is PumpEventStored }
         let batteryUpdates = objects.filter { $0 is OpenAPS_Battery }
+        let overrideUpdates = objects.filter { $0 is OverrideStored }
+        let overrideRunStoredUpdates = objects.filter { $0 is OverrideRunStored }
 
         DispatchQueue.global(qos: .background).async {
             if !glucoseUpdates.isEmpty {
@@ -475,6 +493,12 @@ extension Home.StateModel {
             if !batteryUpdates.isEmpty {
                 self.setupBatteryArray()
             }
+            if !overrideUpdates.isEmpty {
+                self.setupOverrides()
+            }
+            if !overrideRunStoredUpdates.isEmpty {
+                self.setupOverrideRunStored()
+            }
         }
     }
 }
@@ -755,3 +779,114 @@ extension Home.StateModel {
         }
     }
 }
+
+extension Home.StateModel {
+    // Setup Overrides
+    private func setupOverrides() {
+        Task {
+            let ids = await self.fetchOverrides()
+            await updateOverrideArray(with: ids)
+        }
+    }
+
+    private func fetchOverrides() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
+            key: "date",
+            ascending: false
+        )
+
+        return await context.perform {
+            return results.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateOverrideArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let overrideObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OverrideStored
+            }
+
+            overrides = overrideObjects
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the override array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
+        guard let overrideDuration = override.duration as? Double, overrideDuration != 0 else {
+            return TimeInterval(60 * 60 * 24) // one day
+        }
+        return TimeInterval(overrideDuration * 60) // return seconds
+    }
+
+    @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
+        guard let overrideTarget = override.target, overrideTarget != 0 else {
+            return 100 // default
+        }
+        return overrideTarget.decimalValue
+    }
+
+    // Setup expired Overrides
+    private func setupOverrideRunStored() {
+        Task {
+            let ids = await self.fetchOverrideRunStored()
+            await updateOverrideRunStoredArray(with: ids)
+        }
+    }
+
+    private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideRunStored.self,
+            onContext: context,
+            predicate: predicate,
+            key: "startDate",
+            ascending: false
+        )
+
+        return await context.perform {
+            return results.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateOverrideRunStoredArray(with IDs: [NSManagedObjectID]) {
+        do {
+            let overrideObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OverrideRunStored
+            }
+
+            overrideRunStored = overrideObjects
+            debugPrint("expiredOverrides: \(DebuggingIdentifiers.inProgress) \(overrideRunStored)")
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the Override Run Stored array: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
+        await viewContext.perform {
+            do {
+                guard let object = try self.viewContext.existingObject(with: id) as? OverrideStored else { return }
+
+                let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
+                newOverrideRunStored.id = UUID()
+                newOverrideRunStored.startDate = object.date ?? .distantPast
+                newOverrideRunStored.endDate = Date()
+                newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
+                newOverrideRunStored.override = object
+
+                guard self.viewContext.hasChanges else { return }
+                try self.viewContext.save()
+
+            } catch {
+                print(error.localizedDescription)
+            }
+        }
+    }
+}

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

@@ -206,6 +206,8 @@ extension MainChartView {
                 drawFpus()
                 drawBoluses()
                 drawTempTargets()
+                drawActiveOverrides()
+                drawOverrideRunStored()
                 drawForecasts()
                 drawGlucose()
                 drawManualGlucose()
@@ -540,6 +542,48 @@ extension MainChartView {
         }
     }
 
+    private func drawActiveOverrides() -> some ChartContent {
+        ForEach(state.overrides) { override in
+            let start: Date = override.date ?? .distantPast
+            let duration = state.calculateDuration(override: override)
+            let end: Date = start.addingTimeInterval(duration)
+            let target = state.calculateTarget(override: override)
+
+            RuleMark(
+                xStart: .value("Start", start, unit: .second),
+                xEnd: .value("End", end, unit: .second),
+                y: .value("Value", target)
+            )
+            .foregroundStyle(Color.purple.opacity(0.6))
+            .lineStyle(.init(lineWidth: 8))
+//            .annotation(position: .overlay, spacing: 0) {
+//                if let name = override.name {
+//                    Text("\(name)").foregroundStyle(.secondary).font(.footnote)
+//                }
+//            }
+        }
+    }
+
+    private func drawOverrideRunStored() -> some ChartContent {
+        ForEach(state.overrideRunStored) { overrideRunStored in
+            let start: Date = overrideRunStored.startDate ?? .distantPast
+            let end: Date = overrideRunStored.endDate ?? Date()
+            let target = overrideRunStored.target?.decimalValue ?? 100
+            RuleMark(
+                xStart: .value("Start", start, unit: .second),
+                xEnd: .value("End", end, unit: .second),
+                y: .value("Value", target)
+            )
+            .foregroundStyle(Color.purple.opacity(0.4))
+            .lineStyle(.init(lineWidth: 8))
+//            .annotation(position: .bottom, spacing: 0) {
+//                if let name = overrideRunStored.override?.name {
+//                    Text("\(name)").foregroundStyle(.secondary).font(.footnote)
+//                }
+//            }
+        }
+    }
+
     private func drawManualGlucose() -> some ChartContent {
         /// manual glucose mark
         ForEach(state.manualGlucoseFromPersistence) { item in

+ 12 - 5
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -228,6 +228,11 @@ extension Home {
                 } else if newDuration > 0 {
                     durationString =
                         "\((newDuration * 60).formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) s"
+                } else {
+                    /// Do not show the Override anymore
+                    Task {
+                        await state.cancelProfile(withID: latestOverride.objectID)
+                    }
                 }
             }
 
@@ -344,7 +349,8 @@ extension Home {
                     displayXgridLines: $state.displayXgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     thresholdLines: $state.thresholdLines,
-                    isTempTargetActive: $state.isTempTargetActive, state: state
+                    isTempTargetActive: $state.isTempTargetActive,
+                    state: state
                 )
             }
             .padding(.bottom)
@@ -557,6 +563,7 @@ extension Home {
                             Button("Yes", role: .destructive) {
                                 Task {
                                     guard let objectID = latestOverride.first?.objectID else { return }
+                                    await state.saveToOverrideRunStored(withID: objectID)
                                     await state.cancelProfile(withID: objectID)
                                 }
                             }
@@ -786,15 +793,15 @@ extension Home {
                     NavigationStack { OverrideProfilesConfig.RootView(resolver: resolver) }
                         .tabItem {
                             Label(
-                                "Profile",
-                                systemImage: "person.fill"
+                                "Adjustments",
+                                systemImage: "slider.horizontal.2.gobackward"
                             ) }.tag(2)
 
                     NavigationStack(path: self.$settingsPath) {
                         Settings.RootView(resolver: resolver) }
                         .tabItem { Label(
-                            "Menu",
-                            systemImage: "text.justify"
+                            "Settings",
+                            systemImage: "gear"
                         ) }.tag(3)
                 }
                 .tint(Color.tabBar)

+ 11 - 9
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -326,20 +326,22 @@ extension NightscoutConfig {
             }
         }
 
-        func backfillGlucose() {
+        func backfillGlucose() async {
             backfilling = true
-            nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
-                .sink { [weak self] glucose in
-                    guard let self = self else { return }
-                    DispatchQueue.main.async {
-                        self.backfilling = false
-                    }
 
-                    guard glucose.isNotEmpty else { return }
+            let glucose = await nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
+
+            if glucose.isNotEmpty {
+                await MainActor.run {
+                    self.backfilling = false
                     self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
                     self.glucoseStorage.storeGlucose(glucose)
                 }
-                .store(in: &lifetime)
+            } else {
+                await MainActor.run {
+                    self.backfilling = false
+                }
+            }
         }
 
         func delete() {

+ 6 - 2
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -149,8 +149,12 @@ extension NightscoutConfig {
                     }
                 } header: { Text("Local glucose source") }
                 Section {
-                    Button("Backfill glucose") { state.backfillGlucose() }
-                        .disabled(state.url.isEmpty || state.connecting || state.backfilling)
+                    Button("Backfill glucose") {
+                        Task {
+                            await state.backfillGlucose()
+                        }
+                    }
+                    .disabled(state.url.isEmpty || state.connecting || state.backfilling)
                 }
 
                 Section {

+ 3 - 3
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesDataFlow.swift

@@ -5,7 +5,7 @@ enum OverrideProfilesConfig {
     enum Config {}
 
     enum Tab: String, Hashable, Identifiable, CaseIterable {
-        case profiles
+        case overrides
         case tempTargets
 
         var id: String { rawValue }
@@ -13,8 +13,8 @@ enum OverrideProfilesConfig {
         var name: String {
             var name: String = ""
             switch self {
-            case .profiles:
-                name = "Profiles"
+            case .overrides:
+                name = "Overrides"
             case .tempTargets:
                 name = "Temp Targets"
             }

+ 186 - 360
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -3,21 +3,22 @@ import SwiftUI
 
 extension OverrideProfilesConfig {
     final class StateModel: BaseStateModel<Provider> {
+        @Injected() var broadcaster: Broadcaster!
         @Injected() var storage: TempTargetsStorage!
         @Injected() var apsManager: APSManager!
+        @Injected() var overrideStorage: OverrideStorage!
 
-        @Published var percentageProfiles: Double = 100
+        @Published var overrideSliderPercentage: Double = 100
         @Published var isEnabled = false
-        @Published var _indefinite = true
-        @Published var durationProfile: Decimal = 0
+        @Published var indefinite = true
+        @Published var overrideDuration: Decimal = 0
         @Published var target: Decimal = 0
-        @Published var override_target: Bool = false
+        @Published var shouldOverrideTarget: Bool = false
         @Published var smbIsOff: Bool = false
-        @Published var id: String = ""
-        @Published var profileName: String = ""
+        @Published var id = ""
+        @Published var overrideName: String = ""
         @Published var isPreset: Bool = false
-        @Published var profilePresets: [OverrideStored] = []
-        @Published var selection: OverrideStored?
+        @Published var overridePresets: [OverrideStored] = []
         @Published var advancedSettings: Bool = false
         @Published var isfAndCr: Bool = true
         @Published var isf: Bool = true
@@ -29,15 +30,15 @@ extension OverrideProfilesConfig {
         @Published var uamMinutes: Decimal = 0
         @Published var defaultSmbMinutes: Decimal = 0
         @Published var defaultUamMinutes: Decimal = 0
-        @Published var selectedTab: Tab = .profiles
+        @Published var selectedTab: Tab = .overrides
         @Published var activeOverrideName: String = ""
         @Published var currentActiveOverride: OverrideStored?
+        @Published var showOverrideEditSheet = false
 
         var units: GlucoseUnits = .mmolL
 
         // temp target stuff
         @Published var low: Decimal = 0
-        // @Published var target: Decimal = 0
         @Published var high: Decimal = 0
         @Published var durationTT: Decimal = 0
         @Published var date = Date()
@@ -49,12 +50,6 @@ extension OverrideProfilesConfig {
         @Published var hbt: Double = 160
         @Published var didSaveSettings: Bool = false
 
-        private var dateFormatter: DateFormatter {
-            let df = DateFormatter()
-            df.dateFormat = "dd.MM.yy HH:mm"
-            return df
-        }
-
         override func subscribe() {
             setupNotification()
             units = settingsManager.settings.units
@@ -64,6 +59,7 @@ extension OverrideProfilesConfig {
             updateLatestOverrideConfiguration()
             presetsTT = storage.presets()
             maxValue = settingsManager.preferences.autosensMax
+            broadcaster.register(SettingsObserver.self, observer: self)
         }
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
@@ -97,7 +93,6 @@ extension OverrideProfilesConfig.StateModel {
     }
 
     /// determine the actions when the context has changed
-    ///
     /// its done on a background thread and after that the UI gets updated on the main thread
     @objc private func contextDidSave(_ notification: Notification) {
         guard let userInfo = notification.userInfo else { return }
@@ -120,334 +115,207 @@ extension OverrideProfilesConfig.StateModel {
             }
         }
     }
-}
 
-// MARK: - Enact Overrides
-
-extension OverrideProfilesConfig.StateModel {
-    func scheduleOverrideDisabling(for override: OverrideStored) {
-        let now = Date()
-        guard let toCancelDuration = override.duration,
-              let endTime = override.date?
-              .addingTimeInterval(
-                  TimeInterval(truncating: toCancelDuration) *
-                      60
-              ) // ensuring duration is minutes, not seconds!
-        else {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) End time calculation failed")
-            return
-        }
+    // MARK: - Enact Overrides
 
-        debugPrint(
-            "\(DebuggingIdentifiers.inProgress) \(#file) \(#function) Scheduling cancellation at \(endTime) (in \(endTime.timeIntervalSince(now)) seconds)"
-        )
+    func reorderOverride(from source: IndexSet, to destination: Int) {
+        overridePresets.move(fromOffsets: source, toOffset: destination)
 
-        guard endTime > now else {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) End time is in the past or now")
-            return
+        for (index, override) in overridePresets.enumerated() {
+            override.orderPosition = Int16(index + 1)
         }
 
-        let timeInterval = endTime.timeIntervalSince(now)
-
-        DispatchQueue.main.asyncAfter(deadline: .now() + timeInterval) { [weak self] in
-            debugPrint("\(DebuggingIdentifiers.inProgress) \(#file) \(#function) Executing scheduled cancelActiveProfile")
-            self?.cancelActiveProfile()
+        do {
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
+            )
         }
     }
 
-    // Enact Preset
     /// here we only have to update the Boolean Flag 'enabled'
-    @MainActor func enactProfilePreset(withID id: NSManagedObjectID) async {
+    @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
         do {
-            /// get the underlying NSManagedObject of the Profile that should be enabled
-            let profileToEnact = try viewContext.existingObject(with: id) as? OverrideStored
-            profileToEnact?.enabled = true
-            profileToEnact?.date = Date()
+            /// get the underlying NSManagedObject of the Override that should be enabled
+            let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
+            overrideToEnact?.enabled = true
+            overrideToEnact?.date = Date()
 
-            /// Update the 'Cancel Profile' button state
+            /// Update the 'Cancel Override' button state
             isEnabled = true
 
-            /// disable all active Profiles and reset state variables
-            await disableAllActiveProfiles(except: id)
-            await resetStateVariables()
+            /// disable all active Overrides and reset state variables
+            /// do not create a OverrideRunEntry because we only want that if we cancel a running Override, not when enacting a Preset
+            await disableAllActiveOverrides(except: id, createOverrideRunEntry: false)
 
-            if let toSchedule = profileToEnact {
-                scheduleOverrideDisabling(for: toSchedule)
-            }
+            await resetStateVariables()
 
             guard viewContext.hasChanges else { return }
             try viewContext.save()
         } catch {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Profile Preset")
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
         }
     }
-}
 
-// MARK: - Profile (presets) save operations
+    // MARK: - Save the Override that we want to cancel to the OverrideRunStored Entity, then cancel ALL active overrides
 
-extension OverrideProfilesConfig.StateModel {
-    // Saves Profile in a background context
-    /// not a Preset
-    func saveAsProfile() async {
-        await coredataContext.perform { [self] in
-            let newProfile = OverrideStored(context: self.coredataContext)
-            if self.profileName.isNotEmpty {
-                newProfile.name = self.profileName
-            } else {
-                let formattedDate = dateFormatter.string(from: Date())
-                newProfile.name = "Preset <\(formattedDate)>"
-            }
-            newProfile.duration = self.durationProfile as NSDecimalNumber
-            newProfile.indefinite = self._indefinite
-            newProfile.percentage = self.percentageProfiles
-            newProfile.enabled = true
-            newProfile.smbIsOff = self.smbIsOff
-            if self.isPreset {
-                newProfile.isPreset = true
-                newProfile.id = id
-            } else { newProfile.isPreset = false }
-            newProfile.date = Date()
-            if override_target {
-                if units == .mmolL {
-                    target = target.asMgdL
-                }
-                newProfile.target = target as NSDecimalNumber
-            } else { newProfile.target = 0 }
-
-            if advancedSettings {
-                newProfile.advancedSettings = true
-
-                if !isfAndCr {
-                    newProfile.isfAndCr = false
-                    newProfile.isf = isf
-                    newProfile.cr = cr
-                } else { newProfile.isfAndCr = true }
-                if smbIsAlwaysOff {
-                    newProfile.smbIsAlwaysOff = true
-                    newProfile.start = start as NSDecimalNumber
-                    newProfile.end = end as NSDecimalNumber
-                } else { newProfile.smbIsAlwaysOff = false }
-
-                newProfile.smbMinutes = smbMinutes as NSDecimalNumber
-                newProfile.uamMinutes = uamMinutes as NSDecimalNumber
-            }
+    @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
+        // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
+
+        await viewContext.perform {
             do {
-                guard coredataContext.hasChanges else { return }
-                try coredataContext.save()
-                self.scheduleOverrideDisabling(for: newProfile)
+                // Fetch the existing OverrideStored objects from the context
+                let results = try ids.compactMap { id in
+                    try self.viewContext.existingObject(with: id) as? OverrideStored
+                }
+
+                // If there are no results, return early
+                guard !results.isEmpty else { return }
+
+                // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
+                if createOverrideRunEntry {
+                    // Use the first override to create a new OverrideRunStored entry
+                    if let canceledOverride = results.first {
+                        let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
+                        newOverrideRunStored.id = UUID()
+                        newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
+                        newOverrideRunStored.endDate = Date()
+                        newOverrideRunStored
+                            .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                        newOverrideRunStored.override = canceledOverride
+                    }
+                }
+
+                // Disable all override except the one with overrideID
+                for overrideToCancel in results {
+                    if overrideToCancel.objectID != overrideID {
+                        overrideToCancel.enabled = false
+                    }
+                }
+
+                // Save the context if there are changes
+                if self.viewContext.hasChanges {
+                    try self.viewContext.save()
+                }
             } catch {
-                print(error.localizedDescription)
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+                )
             }
         }
     }
 
+    // MARK: - Override (presets) save operations
+
+    // Saves a Custom Override in a background context
+    /// not a Preset
+    func saveCustomOverride() async {
+        let override = Override(
+            name: overrideName,
+            enabled: true,
+            date: Date(),
+            duration: overrideDuration,
+            indefinite: indefinite,
+            percentage: overrideSliderPercentage,
+            smbIsOff: smbIsOff,
+            isPreset: isPreset,
+            id: id,
+            overrideTarget: shouldOverrideTarget,
+            target: target,
+            advancedSettings: advancedSettings,
+            isfAndCr: isfAndCr,
+            isf: isf,
+            cr: cr,
+            smbIsAlwaysOff: smbIsAlwaysOff,
+            start: start,
+            end: end,
+            smbMinutes: smbMinutes,
+            uamMinutes: uamMinutes
+        )
+
+        await overrideStorage.storeOverride(override: override)
+        await resetStateVariables()
+    }
+
     // Save Presets
     /// enabled has to be false, isPreset has to be true
-    func savePreset() async {
-        await coredataContext.perform { [self] in
-            let newOverride = OverrideStored(context: self.coredataContext)
-            newOverride.duration = self.durationProfile as NSDecimalNumber
-            newOverride.indefinite = self._indefinite
-            newOverride.percentage = self.percentageProfiles
-            newOverride.smbIsOff = self.smbIsOff
-            if self.profileName.isNotEmpty {
-                newOverride.name = self.profileName
-            } else {
-                let formattedDate = dateFormatter.string(from: Date())
-                newOverride.name = "Profile \(formattedDate)"
-            }
-            newOverride.isPreset = true
-            newOverride.date = Date()
-            newOverride.enabled = false
-
-            if override_target {
-                newOverride.target = (
-                    units == .mmolL
-                        ? target.asMgdL
-                        : target
-                ) as NSDecimalNumber
-            }
+    func saveOverridePreset() async {
+        let preset = Override(
+            name: overrideName,
+            enabled: false,
+            date: Date(),
+            duration: overrideDuration,
+            indefinite: indefinite,
+            percentage: overrideSliderPercentage,
+            smbIsOff: smbIsOff,
+            isPreset: true,
+            id: id,
+            overrideTarget: shouldOverrideTarget,
+            target: target,
+            advancedSettings: advancedSettings,
+            isfAndCr: isfAndCr,
+            isf: isf,
+            cr: cr,
+            smbIsAlwaysOff: smbIsAlwaysOff,
+            start: start,
+            end: end,
+            smbMinutes: smbMinutes,
+            uamMinutes: uamMinutes
+        )
 
-            if advancedSettings {
-                newOverride.advancedSettings = true
-
-                if !isfAndCr {
-                    newOverride.isfAndCr = false
-                    newOverride.isf = isf
-                    newOverride.cr = cr
-                } else { newOverride.isfAndCr = true }
-                if smbIsAlwaysOff {
-                    newOverride.smbIsAlwaysOff = true
-                    newOverride.start = start as NSDecimalNumber
-                    newOverride.end = end as NSDecimalNumber
-                } else { newOverride.smbIsAlwaysOff = false }
-
-                newOverride.smbMinutes = smbMinutes as NSDecimalNumber
-                newOverride.uamMinutes = uamMinutes as NSDecimalNumber
-            }
-            do {
-                guard coredataContext.hasChanges else { return }
-                try coredataContext.save()
+        await overrideStorage.storeOverride(override: preset)
 
-                /// Custom Notification to update Presets View
-                Foundation.NotificationCenter.default.post(name: .didUpdateOverridePresets, object: nil)
+        // Custom Notification to update Presets View
+        Foundation.NotificationCenter.default.post(name: .didUpdateOverridePresets, object: nil)
 
-                /// prevent showing the current config of the recently added Preset
-                Task {
-                    await resetStateVariables()
-                }
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Preset to Core Data with error: \(error.userInfo)"
-                )
-            }
-        }
+        // Prevent showing the current config of the recently added Preset
+        await resetStateVariables()
     }
-}
 
-// MARK: - Setup Override Presets Array
+    // MARK: - Setup Override Presets Array
 
-extension OverrideProfilesConfig.StateModel {
-    // Fill the array of the Profile Presets to display them in the UI
+    // Fill the array of the Override Presets to display them in the UI
     private func setupOverridePresetsArray() {
         Task {
-            let ids = await self.fetchForProfilePresets()
+            let ids = await self.overrideStorage.fetchForOverridePresets()
             await updateOverridePresetsArray(with: ids)
         }
     }
 
-    /// Returns the NSManagedObjectID of the Override Presets
-    private func fetchForProfilePresets() async -> [NSManagedObjectID] {
-        let result = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideStored.self,
-            onContext: coredataContext,
-            predicate: NSPredicate.allOverridePresets,
-            key: "name",
-            ascending: true
-        )
-
-        return await coredataContext.perform {
-            return result.map(\.objectID)
-        }
-    }
-
     @MainActor private func updateOverridePresetsArray(with IDs: [NSManagedObjectID]) async {
         do {
             let overrideObjects = try IDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? OverrideStored
             }
-            profilePresets = overrideObjects
+            overridePresets = overrideObjects
         } catch {
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
             )
         }
     }
-}
-
-// MARK: - Profile Cancelling
-
-extension OverrideProfilesConfig.StateModel {
-    /// Gets the corresponding NSManagedObjectID of the current active Profile and cancels it
-    func cancelActiveProfile() {
-        Task {
-            let id = await getActiveProfile()
-            await cancelActiveProfile(withID: id)
-        }
-    }
-
-    func getActiveProfile() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideStored.self,
-            onContext: coredataContext,
-            predicate: NSPredicate.lastActiveOverride,
-            key: "date",
-            ascending: false,
-            fetchLimit: 1
-        )
-
-        return await coredataContext.perform {
-            return results.first.map(\.objectID)
-        }
-    }
-
-    @MainActor func cancelActiveProfile(withID id: NSManagedObjectID?) async {
-        guard let id = id else { return }
 
-        return await viewContext.perform {
-            do {
-                let profileToCancel = try self.viewContext.existingObject(with: id) as? OverrideStored
-                profileToCancel?.enabled = false
-
-                /// Update the 'Cancel Profile' button state
-                self.isEnabled = false
+    // MARK: - Override Preset Deletion
 
-                guard self.viewContext.hasChanges else { return }
-                try self.viewContext.save()
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile with error: \(error.localizedDescription)"
-                )
-            }
-        }
+    func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
+        await overrideStorage.deleteOverridePreset(objectID)
+        // Custom Notification to update Presets View
+        Foundation.NotificationCenter.default.post(name: .didUpdateOverridePresets, object: nil)
     }
 
-    /// Gets the corresponding NSManagedObjectIDs of all active Profiles and cancels them
-    @MainActor func disableAllActiveProfiles(except profileID: NSManagedObjectID? = nil) async {
-        /// get all NSManagedObject IDs of all active Profiles
-        let ids = await loadLatestOverrideConfigurations(fetchLimit: 0) /// 0 = no fetch limit
+    // MARK: - Setup the State variables with the last Override configuration
 
-        /// end all active profiles
-        do {
-            let results = try ids.compactMap { id in
-                try viewContext.existingObject(with: id) as? OverrideStored
-            }
-
-            for profile in results {
-                if profile.objectID != profileID {
-                    profile.enabled = false
-                }
-            }
-
-            try await viewContext.perform {
-                guard self.viewContext.hasChanges else { return }
-                try self.viewContext.save()
-            }
-        } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Profiles with error: \(error.localizedDescription)"
-            )
-        }
-    }
-}
-
-// MARK: - Setup the State variables with the last Override configuration
-
-extension OverrideProfilesConfig.StateModel {
     /// First get the latest Overrides corresponding NSManagedObjectID with a background fetch
     /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
-    /// This also needs to be called when we cancel a Profile via the Home View to update the State of the Button for this case
+    /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
     func updateLatestOverrideConfiguration() {
         Task {
-            let id = await loadLatestOverrideConfigurations(fetchLimit: 1)
-
+            let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
             await updateLatestOverrideConfigurationOfState(from: id)
-            await setCurrentOverrideName(from: id)
-        }
-    }
-
-    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideStored.self,
-            onContext: coredataContext,
-            predicate: NSPredicate.lastActiveOverride,
-            key: "date",
-            ascending: false,
-            fetchLimit: fetchLimit
-        )
-
-        return await coredataContext.perform {
-            return results.map(\.objectID)
+            await setCurrentOverride(from: id)
         }
     }
 
@@ -468,8 +336,8 @@ extension OverrideProfilesConfig.StateModel {
         }
     }
 
-    /// Sets the current active Preset name to show in the UI
-    @MainActor func setCurrentOverrideName(from IDs: [NSManagedObjectID]) async {
+    // Sets the current active Preset name to show in the UI
+    @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
         do {
             guard let firstID = IDs.first else {
                 activeOverrideName = "Custom Override"
@@ -478,12 +346,8 @@ extension OverrideProfilesConfig.StateModel {
             }
 
             if let overrideToEdit = try viewContext.existingObject(with: firstID) as? OverrideStored {
-                if overrideToEdit.isPreset {
-                    await handlePresetOverride(overrideToEdit)
-                } else {
-                    currentActiveOverride = overrideToEdit
-                    activeOverrideName = overrideToEdit.name ?? "Custom Override"
-                }
+                currentActiveOverride = overrideToEdit
+                activeOverrideName = overrideToEdit.name ?? "Custom Override"
             }
         } catch {
             debugPrint(
@@ -492,54 +356,42 @@ extension OverrideProfilesConfig.StateModel {
         }
     }
 
-    @MainActor private func handlePresetOverride(_ overrideToEdit: OverrideStored) async {
+    @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
+        // We get the current active Preset by using currentActiveOverride which can either be a Preset or a custom Override
+        guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset == true else { return }
+
+        // Copy the current Override-Preset to not edit the underlying Preset
+        await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
+
+        // Cancel the duplicated Override
+        /// As we are on the Main Thread already we don't need to cancel via the objectID in this case
         do {
-            await copyOverride(overrideToEdit)
-            await cancelActiveProfile(withID: overrideToEdit.objectID)
-
-            let ids = await loadLatestOverrideConfigurations(fetchLimit: 1)
-            if let copiedID = ids.first,
-               let copiedOverride = try viewContext.existingObject(with: copiedID) as? OverrideStored
-            {
-                currentActiveOverride = copiedOverride
-                activeOverrideName = copiedOverride.name ?? "Custom Override"
+            try await viewContext.perform {
+                overridePresetToDuplicate.enabled = false
+
+                guard self.viewContext.hasChanges else { return }
+                try self.viewContext.save()
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to handle preset override with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
             )
         }
     }
-}
 
-// MARK: - Profile Preset Deletion
+    // MARK: - Helper functions for Overrides
 
-extension OverrideProfilesConfig.StateModel {
-    /// marked as MainActor to be able to publish changes from the background
-    /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-    @MainActor func invokeProfilePresetDeletion(_ objectID: NSManagedObjectID) {
-        Task {
-            await deleteProfile(objectID)
-        }
-    }
-
-    private func deleteProfile(_ objectID: NSManagedObjectID) async {
-        CoreDataStack.shared.deleteObject(identifiedBy: objectID)
-    }
-}
-
-// MARK: - Helper functions for Overrides
-
-extension OverrideProfilesConfig.StateModel {
     @MainActor func resetStateVariables() async {
-        durationProfile = 0
-        _indefinite = true
-        percentageProfiles = 100
+        id = ""
+
+        overrideDuration = 0
+        indefinite = true
+        overrideSliderPercentage = 100
 
         advancedSettings = false
         smbIsOff = false
-        profileName = ""
-        override_target = false
+        overrideName = ""
+        shouldOverrideTarget = false
         isf = true
         cr = true
         isfAndCr = true
@@ -550,41 +402,6 @@ extension OverrideProfilesConfig.StateModel {
         uamMinutes = defaultUamMinutes
         target = 0
     }
-
-    // Copy the current Override if it is a running Preset
-    /// otherwise we would edit the current running Preset
-    @MainActor private func copyOverride(_ override: OverrideStored) async {
-        let newOverride = OverrideStored(context: viewContext)
-        newOverride.duration = override.duration
-        newOverride.indefinite = override.indefinite
-        newOverride.percentage = override.percentage
-        newOverride.smbIsOff = override.smbIsOff
-        newOverride.name = override.name
-        newOverride.isPreset = false // no Preset
-        newOverride.date = Date()
-        newOverride.enabled = override.enabled
-        newOverride.target = override.target
-        newOverride.advancedSettings = override.advancedSettings
-        newOverride.isfAndCr = override.isfAndCr
-        newOverride.isf = override.isf
-        newOverride.cr = override.cr
-        newOverride.smbIsAlwaysOff = override.smbIsAlwaysOff
-        newOverride.start = override.start
-        newOverride.end = override.end
-        newOverride.smbMinutes = override.smbMinutes
-        newOverride.uamMinutes = override.uamMinutes
-
-        await viewContext.perform {
-            do {
-                guard self.viewContext.hasChanges else { return }
-                try self.viewContext.save()
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to copy Override with error: \(error.userInfo)"
-                )
-            }
-        }
-    }
 }
 
 // MARK: - TEMP TARGET
@@ -781,3 +598,12 @@ extension OverrideProfilesConfig.StateModel {
         return Decimal(Double(target))
     }
 }
+
+extension OverrideProfilesConfig.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+        defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
+        defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
+        maxValue = settingsManager.preferences.autosensMax
+    }
+}

+ 30 - 29
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/AddProfileForm.swift

@@ -1,7 +1,7 @@
 import Foundation
 import SwiftUI
 
-struct AddProfileForm: View {
+struct AddOverrideForm: View {
     @Environment(\.presentationMode) var presentationMode
     @StateObject var state: OverrideProfilesConfig.StateModel
     @State private var isEditing = false
@@ -50,19 +50,19 @@ struct AddProfileForm: View {
     var body: some View {
         NavigationView {
             Form {
-                addProfile()
+                addOverride()
             }.scrollContentBackground(.hidden).background(color)
-                .navigationTitle("Add Profile")
+                .navigationTitle("Add Override")
                 .navigationBarItems(trailing: Button("Cancel") {
                     presentationMode.wrappedValue.dismiss()
                 })
         }
     }
 
-    @ViewBuilder private func addProfile() -> some View {
+    @ViewBuilder private func addOverride() -> some View {
         Section {
             VStack {
-                TextField("Name", text: $state.profileName)
+                TextField("Name", text: $state.overrideName)
             }
         } header: {
             Text("Name")
@@ -71,15 +71,15 @@ struct AddProfileForm: View {
         Section {
             VStack {
                 Spacer()
-                Text("\(state.percentageProfiles.formatted(.number)) %")
+                Text("\(state.overrideSliderPercentage.formatted(.number)) %")
                     .foregroundColor(
                         state
-                            .percentageProfiles >= 130 ? .red :
+                            .overrideSliderPercentage >= 130 ? .red :
                             (isEditing ? .orange : Color.tabBar)
                     )
                     .font(.largeTitle)
                 Slider(
-                    value: $state.percentageProfiles,
+                    value: $state.overrideSliderPercentage,
                     in: 10 ... 200,
                     step: 1,
                     onEditingChanged: { editing in
@@ -87,24 +87,24 @@ struct AddProfileForm: View {
                     }
                 )
                 Spacer()
-                Toggle(isOn: $state._indefinite) {
+                Toggle(isOn: $state.indefinite) {
                     Text("Enable indefinitely")
                 }
             }
-            if !state._indefinite {
+            if !state.indefinite {
                 HStack {
                     Text("Duration")
-                    TextFieldWithToolBar(text: $state.durationProfile, placeholder: "0", numberFormatter: formatter)
+                    TextFieldWithToolBar(text: $state.overrideDuration, placeholder: "0", numberFormatter: formatter)
                     Text("minutes").foregroundColor(.secondary)
                 }
             }
 
             HStack {
-                Toggle(isOn: $state.override_target) {
+                Toggle(isOn: $state.shouldOverrideTarget) {
                     Text("Override Profile Target")
                 }
             }
-            if state.override_target {
+            if state.shouldOverrideTarget {
                 HStack {
                     Text("Target Glucose")
                     TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
@@ -180,23 +180,23 @@ struct AddProfileForm: View {
 
     private var startAndSaveProfiles: some View {
         HStack {
-            Button("Start new Profile") {
+            Button("Start new Override") {
                 showAlert.toggle()
 
-                alertString = "\(state.percentageProfiles.formatted(.number)) %, " +
+                alertString = "\(state.overrideSliderPercentage.formatted(.number)) %, " +
                     (
-                        state.durationProfile > 0 || !state
-                            ._indefinite ?
+                        state.overrideDuration > 0 || !state
+                            .indefinite ?
                             (
                                 state
-                                    .durationProfile
+                                    .overrideDuration
                                     .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
                                     " min."
                             ) :
                             NSLocalizedString(" infinite duration.", comment: "")
                     ) +
                     (
-                        (state.target == 0 || !state.override_target) ? "" :
+                        (state.target == 0 || !state.shouldOverrideTarget) ? "" :
                             (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
                     )
                     +
@@ -212,7 +212,7 @@ struct AddProfileForm: View {
                     "\n\n"
                     +
                     NSLocalizedString(
-                        "Starting this override will change your Profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Profile” will start your new profile or edit your current active profile.",
+                        "Starting this override will change your profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Override” will start your new Override or edit your current active Override.",
                         comment: ""
                     )
             }
@@ -221,15 +221,15 @@ struct AddProfileForm: View {
             .font(.callout)
             .controlSize(.mini)
             .alert(
-                "Start Profile",
+                "Start Override",
                 isPresented: $showAlert,
                 actions: {
                     Button("Cancel", role: .cancel) { state.isEnabled = false }
-                    Button("Start Profile", role: .destructive) {
+                    Button("Start Override", role: .destructive) {
                         Task {
-                            if state._indefinite { state.durationProfile = 0 }
+                            if state.indefinite { state.overrideDuration = 0 }
                             state.isEnabled.toggle()
-                            await state.saveAsProfile()
+                            await state.saveCustomOverride()
                             await state.resetStateVariables()
                             dismiss()
                         }
@@ -241,11 +241,11 @@ struct AddProfileForm: View {
             )
             Button {
                 Task {
-                    await state.savePreset()
+                    await state.saveOverridePreset()
                     dismiss()
                 }
             }
-            label: { Text("Save as Profile") }
+            label: { Text("Save as Override") }
                 .tint(.orange)
                 .frame(maxWidth: .infinity, alignment: .trailing)
                 .buttonStyle(BorderlessButtonStyle())
@@ -256,12 +256,13 @@ struct AddProfileForm: View {
 
     private func unChanged() -> Bool {
         let isChanged = (
-            state.percentageProfiles == 100 && !state.override_target && !state.smbIsOff && !state
+            state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && !state
                 .advancedSettings
         ) ||
-            (!state._indefinite && state.durationProfile == 0) || (state.override_target && state.target == 0) ||
+            (!state.indefinite && state.overrideDuration == 0) || (state.shouldOverrideTarget && state.target == 0) ||
             (
-                state.percentageProfiles == 100 && !state.override_target && !state.smbIsOff && state.isf && state.cr && state
+                state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && state.isf && state
+                    .cr && state
                     .smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
             )
 

+ 68 - 64
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/EditProfileForm.swift

@@ -1,9 +1,8 @@
 import Foundation
 import SwiftUI
 
-struct EditProfileForm: View {
-//    @Injected() var settingsManager: SettingsManager!
-    @ObservedObject var profile: OverrideStored
+struct EditOverrideForm: View {
+    @ObservedObject var override: OverrideStored
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
     @StateObject var state: OverrideProfilesConfig.StateModel
@@ -28,25 +27,28 @@ struct EditProfileForm: View {
     @State private var isEditing = false
     @State private var target_override = false
 
-    init(profile: OverrideStored, state: OverrideProfilesConfig.StateModel) {
-        self.profile = profile
+    init(overrideToEdit: OverrideStored, state: OverrideProfilesConfig.StateModel) {
+        override = overrideToEdit
         _state = StateObject(wrappedValue: state)
-        _name = State(initialValue: profile.name ?? "")
-        _percentage = State(initialValue: profile.percentage)
-        _indefinite = State(initialValue: profile.indefinite)
-        _duration = State(initialValue: profile.duration?.decimalValue ?? 0)
-        _target = State(initialValue: profile.target?.decimalValue)
-        _target_override = State(initialValue: profile.target?.decimalValue != 0)
-        _advancedSettings = State(initialValue: profile.advancedSettings)
-        _smbIsOff = State(initialValue: profile.smbIsOff)
-        _smbIsAlwaysOff = State(initialValue: profile.smbIsAlwaysOff)
-        _start = State(initialValue: profile.start?.decimalValue)
-        _end = State(initialValue: profile.end?.decimalValue)
-        _isfAndCr = State(initialValue: profile.isfAndCr)
-        _isf = State(initialValue: profile.isf)
-        _cr = State(initialValue: profile.cr)
-        _smbMinutes = State(initialValue: profile.smbMinutes?.decimalValue)
-        _uamMinutes = State(initialValue: profile.uamMinutes?.decimalValue)
+        _name = State(initialValue: overrideToEdit.name ?? "")
+        _percentage = State(initialValue: overrideToEdit.percentage)
+        _indefinite = State(initialValue: overrideToEdit.indefinite)
+        _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
+        _target = State(
+            initialValue: state.units == .mgdL ? overrideToEdit.target?.decimalValue : overrideToEdit.target?
+                .decimalValue.asMmolL
+        )
+        _target_override = State(initialValue: overrideToEdit.target?.decimalValue != 0)
+        _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
+        _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
+        _smbIsAlwaysOff = State(initialValue: overrideToEdit.smbIsAlwaysOff)
+        _start = State(initialValue: overrideToEdit.start?.decimalValue)
+        _end = State(initialValue: overrideToEdit.end?.decimalValue)
+        _isfAndCr = State(initialValue: overrideToEdit.isfAndCr)
+        _isf = State(initialValue: overrideToEdit.isf)
+        _cr = State(initialValue: overrideToEdit.cr)
+        _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
+        _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
     }
 
     var color: LinearGradient {
@@ -86,12 +88,12 @@ struct EditProfileForm: View {
     var body: some View {
         NavigationView {
             Form {
-                editProfile()
+                editOverride()
 
                 saveButton
 
             }.scrollContentBackground(.hidden).background(color)
-                .navigationTitle("Edit Profile")
+                .navigationTitle("Edit Override")
                 .navigationBarTitleDisplayMode(.inline)
                 .navigationBarItems(leading: Button("Close") {
                     presentationMode.wrappedValue.dismiss()
@@ -105,8 +107,8 @@ struct EditProfileForm: View {
         }
     }
 
-    @ViewBuilder private func editProfile() -> some View {
-        if profile.name != nil {
+    @ViewBuilder private func editOverride() -> some View {
+        if override.name != nil {
             Section {
                 VStack {
                     TextField("Name", text: $name)
@@ -122,7 +124,7 @@ struct EditProfileForm: View {
                 Text("\(percentage.formatted(.number)) %")
                     .foregroundColor(
                         state
-                            .percentageProfiles >= 130 ? .red :
+                            .overrideSliderPercentage >= 130 ? .red :
                             (isEditing ? .orange : Color.tabBar)
                     )
                     .font(.largeTitle)
@@ -156,7 +158,7 @@ struct EditProfileForm: View {
 
             HStack {
                 Toggle(isOn: $target_override) {
-                    Text("Override Profile Target")
+                    Text("Override Override Target")
                 }.onChange(of: target_override) { _ in
                     hasChanges = true
                 }
@@ -165,7 +167,9 @@ struct EditProfileForm: View {
                 HStack {
                     Text("Target Glucose")
                     TextFieldWithToolBar(text: Binding(
-                        get: { target ?? 0 },
+                        get: {
+                            target ?? 0
+                        },
                         set: {
                             target = $0
                             hasChanges = true
@@ -277,11 +281,11 @@ struct EditProfileForm: View {
             Button(action: {
                 saveChanges()
                 do {
-                    try profile.managedObjectContext?.save()
+                    try override.managedObjectContext?.save()
                     hasChanges = false
                     presentationMode.wrappedValue.dismiss()
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Profile")
+                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
                 }
             }, label: {
                 Text("Save")
@@ -295,47 +299,47 @@ struct EditProfileForm: View {
     }
 
     private func saveChanges() {
-        if !profile.isPreset, hasChanges, name == (profile.name ?? "") {
-            profile.name = "Custom Override"
+        if !override.isPreset, hasChanges, name == (override.name ?? "") {
+            override.name = "Custom Override"
         } else {
-            profile.name = name
+            override.name = name
         }
-        profile.percentage = percentage
-        profile.indefinite = indefinite
-        profile.duration = NSDecimalNumber(decimal: duration)
+        override.percentage = percentage
+        override.indefinite = indefinite
+        override.duration = NSDecimalNumber(decimal: duration)
         if target_override {
-            profile.target = target.map { NSDecimalNumber(decimal: $0) }
+            override.target = target.map {
+                state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0) }
         } else {
-            profile.target = 0
+            override.target = 0
         }
-        profile.advancedSettings = advancedSettings
-        profile.smbIsOff = smbIsOff
-        profile.smbIsAlwaysOff = smbIsAlwaysOff
-        profile.start = start.map { NSDecimalNumber(decimal: $0) }
-        profile.end = end.map { NSDecimalNumber(decimal: $0) }
-        profile.isfAndCr = isfAndCr
-        profile.isf = isf
-        profile.cr = cr
-        profile.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
-        profile.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
-        state.scheduleOverrideDisabling(for: profile)
+        override.advancedSettings = advancedSettings
+        override.smbIsOff = smbIsOff
+        override.smbIsAlwaysOff = smbIsAlwaysOff
+        override.start = start.map { NSDecimalNumber(decimal: $0) }
+        override.end = end.map { NSDecimalNumber(decimal: $0) }
+        override.isfAndCr = isfAndCr
+        override.isf = isf
+        override.cr = cr
+        override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
+        override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
     }
 
     private func resetValues() {
-        name = profile.name ?? ""
-        percentage = profile.percentage
-        indefinite = profile.indefinite
-        duration = profile.duration?.decimalValue ?? 0
-        target = profile.target?.decimalValue
-        advancedSettings = profile.advancedSettings
-        smbIsOff = profile.smbIsOff
-        smbIsAlwaysOff = profile.smbIsAlwaysOff
-        start = profile.start?.decimalValue
-        end = profile.end?.decimalValue
-        isfAndCr = profile.isfAndCr
-        isf = profile.isf
-        cr = profile.cr
-        smbMinutes = profile.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
-        uamMinutes = profile.uamMinutes?.decimalValue ?? state.defaultUamMinutes
+        name = override.name ?? ""
+        percentage = override.percentage
+        indefinite = override.indefinite
+        duration = override.duration?.decimalValue ?? 0
+        target = override.target?.decimalValue
+        advancedSettings = override.advancedSettings
+        smbIsOff = override.smbIsOff
+        smbIsAlwaysOff = override.smbIsAlwaysOff
+        start = override.start?.decimalValue
+        end = override.end?.decimalValue
+        isfAndCr = override.isfAndCr
+        isf = override.isf
+        cr = override.cr
+        smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
+        uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
     }
 }

+ 60 - 37
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -9,12 +9,11 @@ extension OverrideProfilesConfig {
         @StateObject var state = StateModel()
 
         @State private var isEditing = false
-        @State private var showProfileCreationSheet = false
+        @State private var showOverrideCreationSheet = false
         @State private var showingDetail = false
         @State private var showCheckmark: Bool = false
         @State private var selectedPresetID: String?
-        @State private var showOverrideEditSheet = false
-        @State private var selectedProfile: OverrideStored?
+        @State private var selectedOverride: OverrideStored?
         // temp targets
         @State private var isPromptPresented = false
         @State private var isRemoveAlertPresented = false
@@ -74,21 +73,21 @@ extension OverrideProfilesConfig {
 
                 Form {
                     switch state.selectedTab {
-                    case .profiles: profiles()
+                    case .overrides: overrides()
                     case .tempTargets: tempTargets() }
                 }.scrollContentBackground(.hidden).background(color)
                     .onAppear(perform: configureView)
-                    .navigationBarTitle("Profiles")
+                    .navigationBarTitle("Adjustments")
                     .navigationBarTitleDisplayMode(.large)
                     .toolbar {
                         ToolbarItem(placement: .topBarTrailing) {
                             switch state.selectedTab {
-                            case .profiles:
+                            case .overrides:
                                 Button(action: {
-                                    showProfileCreationSheet = true
+                                    showOverrideCreationSheet = true
                                 }, label: {
                                     HStack {
-                                        Text("Add Profile")
+                                        Text("Add Override")
                                         Image(systemName: "plus")
                                     }
                                 })
@@ -97,69 +96,82 @@ extension OverrideProfilesConfig {
                             }
                         }
                     }
-                    .sheet(isPresented: $showOverrideEditSheet, onDismiss: {
+                    .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
                         Task {
                             await state.resetStateVariables()
-                            showOverrideEditSheet = false
+                            state.showOverrideEditSheet = false
                         }
 
                     }) {
-                        if let profile = selectedProfile {
-                            EditProfileForm(profile: profile, state: state)
+                        if let override = selectedOverride {
+                            EditOverrideForm(overrideToEdit: override, state: state)
                         }
                     }
-                    .sheet(isPresented: $showProfileCreationSheet, onDismiss: {
+                    .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
                         Task {
                             await state.resetStateVariables()
-                            showProfileCreationSheet = false
+                            showOverrideCreationSheet = false
                         }
                     }) {
-                        AddProfileForm(state: state)
+                        AddOverrideForm(state: state)
                     }
             }.background(color)
         }
 
-        @ViewBuilder func profiles() -> some View {
-            if state.profilePresets.isNotEmpty {
+        @ViewBuilder func overrides() -> some View {
+            if state.overridePresets.isNotEmpty {
                 overridePresets
-
-                if state.isEnabled, state.activeOverrideName.isNotEmpty {
-                    currentActiveOverride
-                }
             } else {
                 defaultText
             }
-            cancelProfileButton
+
+            if state.isEnabled, state.activeOverrideName.isNotEmpty {
+                currentActiveOverride
+            }
+
+            if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
+                cancelOverrideButton
+            }
         }
 
         private var defaultText: some View {
-            Text("Add Preset or Override by tapping the '+'")
+            Section {} header: {
+                Text("Add Preset or Override by tapping the '+'").foregroundStyle(.secondary)
+            }
         }
 
         private var overridePresets: some View {
             Section {
-                ForEach(state.profilePresets) { preset in
-                    profilesView(for: preset)
+                ForEach(state.overridePresets) { preset in
+                    overridesView(for: preset)
                         .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                             Button(role: .none) {
-                                state.invokeProfilePresetDeletion(preset.objectID)
+                                Task {
+                                    await state.invokeOverridePresetDeletion(preset.objectID)
+                                }
                             } label: {
                                 Label("Delete", systemImage: "trash")
                                     .tint(.red)
                             }
                             Button(action: {
-                                selectedProfile = preset
-                                showOverrideEditSheet = true
+                                // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
+                                selectedOverride = preset
+                                state.showOverrideEditSheet = true
                             }, label: {
                                 Label("Edit", systemImage: "pencil")
                                     .tint(.blue)
                             })
                         }
-                }.listRowBackground(Color.chart)
+                }
+                .onMove(perform: state.reorderOverride)
+                .listRowBackground(Color.chart)
             } header: {
                 Text("Presets")
             } footer: {
-                Text("Swipe left to edit or delete a Profile Preset")
+                HStack {
+                    Image(systemName: "hand.draw.fill")
+                    Text("Swipe left to edit or delete an override preset. Drag, hold and drop to reorder a preset.")
+                }
             }
         }
 
@@ -174,20 +186,31 @@ extension OverrideProfilesConfig {
                 }
                 .contentShape(Rectangle())
                 .onTapGesture {
-                    selectedProfile = state.currentActiveOverride
-                    showOverrideEditSheet = true
+                    Task {
+                        /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
+                        /// The currentActiveOverride variable in the State will update automatically via MOC notification
+                        await state.duplicateOverridePresetAndCancelPreviousOverride()
+
+                        /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
+                        selectedOverride = state.currentActiveOverride
+
+                        /// Now we can show the Edit sheet
+                        state.showOverrideEditSheet = true
+                    }
                 }
             }
             .listRowBackground(Color.blue.opacity(0.2))
         }
 
-        private var cancelProfileButton: some View {
+        private var cancelOverrideButton: some View {
             Button(action: {
                 Task {
-                    await state.disableAllActiveProfiles()
+                    // Save cancelled Override in OverrideRunStored Entity
+                    // Cancel ALL active Override
+                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
                 }
             }, label: {
-                Text("Cancel Profile")
+                Text("Cancel Override")
 
             })
                 .frame(maxWidth: .infinity, alignment: .center)
@@ -425,7 +448,7 @@ extension OverrideProfilesConfig {
             })
         }
 
-        @ViewBuilder private func profilesView(for preset: OverrideStored) -> some View {
+        @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
             let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
                 .asMmolL : (preset.target ?? 0) as Decimal
             let duration = (preset.duration ?? 0) as Decimal
@@ -476,7 +499,7 @@ extension OverrideProfilesConfig {
                         .onTapGesture {
                             Task {
                                 let objectID = preset.objectID
-                                await state.enactProfilePreset(withID: objectID)
+                                await state.enactOverridePreset(withID: objectID)
                                 state.hideModal()
                                 showCheckmark.toggle()
                                 selectedPresetID = preset.id

+ 1 - 1
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -170,7 +170,7 @@ extension Settings {
                     ShareSheet(activityItems: state.logItems())
                 }
                 .onAppear(perform: configureView)
-                .navigationTitle("Menu")
+                .navigationTitle("Settings")
                 .navigationBarTitleDisplayMode(.large)
                 .onDisappear(perform: { state.uploadProfileAndSettings(false) })
                 .screenNavigation(self)

+ 34 - 17
FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -5,29 +5,46 @@ import Foundation
 @available(iOS 16.2, *)
 extension LiveActivityBridge {
     func fetchAndMapGlucose() async {
+        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateForSixHoursAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 72
+        )
         await context.perform {
-            self.glucoseFromPersistence = CoreDataStack.shared.fetchEntities(
-                ofType: GlucoseStored.self,
-                onContext: self.context,
-                predicate: NSPredicate.predicateForSixHoursAgo,
-                key: "date",
-                ascending: false,
-                fetchLimit: 72
-            ).map { GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum) }
+            self.glucoseFromPersistence = result
+                .map { GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum) }
         }
     }
 
     func fetchAndMapDetermination() async {
+        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate.enactedDetermination,
+            key: "deliverAt",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["iob", "cob", "deliverAt"]
+        )
         await context.perform {
-            self.determination = CoreDataStack.shared.fetchEntities(
-                ofType: OrefDetermination.self,
-                onContext: self.context,
-                predicate: NSPredicate.enactedDetermination,
-                key: "deliverAt",
-                ascending: false,
-                fetchLimit: 1,
-                propertiesToFetch: ["iob", "cob", "deliverAt"]
-            ).first.map { DeterminationData(cob: Int($0.cob), iob: $0.iob?.decimalValue ?? 0) }
+            self.determination = result.first.map { DeterminationData(cob: Int($0.cob), iob: $0.iob?.decimalValue ?? 0) }
+        }
+    }
+
+    func fetchAndMapOverride() async {
+        let result = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateForOneDayAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        )
+        await context.perform {
+            self.isOverridesActive = result.first.map { OverrideData(isActive: $0.enabled) }
         }
     }
 }

+ 5 - 0
FreeAPS/Sources/Services/LiveActivity/Data/OverrideData.swift

@@ -0,0 +1,5 @@
+import Foundation
+
+struct OverrideData {
+    let isActive: Bool
+}

+ 5 - 2
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -38,7 +38,8 @@ extension LiveActivityAttributes.ContentState {
         mmol: Bool,
         chart: [GlucoseData],
         settings: FreeAPSSettings,
-        determination: DeterminationData?
+        determination: DeterminationData?,
+        override: OverrideData?
     ) {
         let glucose = bg.glucose
         let formattedBG = Self.formatGlucose(Int(glucose), mmol: mmol, forceSign: false)
@@ -81,6 +82,7 @@ extension LiveActivityAttributes.ContentState {
         let iob = determination?.iob ?? 0
         let lockScreenView = settings.lockScreenView.displayName
         let unit = settings.units == .mmolL ? " mmol/L" : " mg/dL"
+        let isOverrideActive = override?.isActive ?? false
 
         self.init(
             bg: formattedBG,
@@ -95,7 +97,8 @@ extension LiveActivityAttributes.ContentState {
             cob: Decimal(cob),
             iob: iob as Decimal,
             lockScreenView: lockScreenView,
-            unit: unit
+            unit: unit,
+            isOverrideActive: isOverrideActive
         )
     }
 }

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

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

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

@@ -41,6 +41,7 @@ import UIKit
     private var currentActivity: ActiveActivity?
     private var latestGlucose: GlucoseData?
     var glucoseFromPersistence: [GlucoseData]?
+    var isOverridesActive: OverrideData?
 
     let context = CoreDataStack.shared.newTaskContext()
 
@@ -77,6 +78,10 @@ import UIKit
             // Fetch and map Determination to DeterminationData struct
             await fetchAndMapDetermination()
 
+            // Fetch and map Override to OverrideData struct
+            /// to show if there is an active Override
+            await fetchAndMapOverride()
+
             // Push the update to the Live Activity
             glucoseDidUpdate(glucoseFromPersistence ?? [])
         }
@@ -148,7 +153,8 @@ import UIKit
                         cob: 0,
                         iob: 0,
                         lockScreenView: "Simple",
-                        unit: "--"
+                        unit: "--",
+                        isOverrideActive: false
                     ),
                     staleDate: Date.now.addingTimeInterval(60)
                 )
@@ -214,7 +220,8 @@ extension LiveActivityBridge {
                 mmol: settings.units == .mmolL,
                 chart: glucose,
                 settings: settings,
-                determination: determination
+                determination: determination,
+                override: isOverridesActive
             )
 
             if let content = content {

+ 127 - 73
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -37,8 +37,8 @@ extension NightscoutAPI {
     func checkConnection() -> AnyPublisher<Void, Swift.Error> {
         struct Check: Codable, Equatable {
             var eventType = "Note"
-            var enteredBy = "iAPS"
-            var notes = "iAPS connected"
+            var enteredBy = "Trio"
+            var notes = "Trio connected"
         }
         let check = Check()
         var request = URLRequest(url: url.appendingPathComponent(Config.treatmentsPath))
@@ -57,7 +57,7 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
-    func fetchLastGlucose(sinceDate: Date? = nil) -> AnyPublisher<[BloodGlucose], Swift.Error> {
+    func fetchLastGlucose(sinceDate: Date? = nil) async throws -> [BloodGlucose] {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
@@ -72,7 +72,11 @@ extension NightscoutAPI {
             components.queryItems?.append(dateItem)
         }
 
-        var request = URLRequest(url: components.url!)
+        guard let url = components.url else {
+            throw URLError(.badURL)
+        }
+
+        var request = URLRequest(url: url)
         request.allowsConstrainedNetworkAccess = false
         request.timeoutInterval = Config.timeout
 
@@ -80,22 +84,18 @@ extension NightscoutAPI {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
 
-        return service.run(request)
-            .retry(Config.retryCount)
-            .decode(type: [BloodGlucose].self, decoder: JSONCoding.decoder)
-            .catch { error -> AnyPublisher<[BloodGlucose], Swift.Error> in
-                warning(.nightscout, "Glucose fetching error: \(error.localizedDescription)")
-                return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
-            }
-            .map { glucose in
-                glucose
-                    .map {
-                        var reading = $0
-                        reading.glucose = $0.sgv
-                        return reading
-                    }
+        do {
+            let (data, _) = try await URLSession.shared.data(for: request)
+            let glucose = try JSONCoding.decoder.decode([BloodGlucose].self, from: data)
+            return glucose.map {
+                var reading = $0
+                reading.glucose = $0.sgv
+                return reading
             }
-            .eraseToAnyPublisher()
+        } catch {
+            warning(.nightscout, "Glucose fetching error: \(error.localizedDescription)")
+            return []
+        }
     }
 
     func fetchCarbs(sinceDate: Date? = nil) -> AnyPublisher<[CarbsEntry], Swift.Error> {
@@ -112,7 +112,7 @@ extension NightscoutAPI {
             ),
             URLQueryItem(
                 name: "find[enteredBy][$ne]",
-                value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
+                value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
             )
         ]
         if let date = sinceDate {
@@ -141,26 +141,15 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
-    func deleteCarbs(_ treatment: DataTable.Treatment) -> AnyPublisher<Void, Swift.Error> {
+    func deleteCarbs(withId id: String) async throws {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
 
-        var arguments = "find[id][$eq]"
-        if treatment.isFPU ?? false {
-            arguments = "find[fpuID][$eq]"
-        }
-        let value = !(treatment.isFPU ?? false) ? treatment.id : (treatment.fpuID ?? "")
-
         components.queryItems = [
-            // Removed below because it prevented all futire entries to be deleted. Don't know why?
-            /* URLQueryItem(name: "find[carbs][$exists]", value: "true"), */
-            URLQueryItem(
-                name: arguments,
-                value: value
-            )
+            URLQueryItem(name: "find[id][$eq]", value: id)
         ]
 
         var request = URLRequest(url: components.url!)
@@ -172,27 +161,30 @@ extension NightscoutAPI {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
 
-        return service.run(request)
-            .retry(Config.retryCount)
-            .map { _ in () }
-            .eraseToAnyPublisher()
+        let (_, response) = try await URLSession.shared.data(for: request)
+
+        guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
+            throw URLError(.badServerResponse)
+        }
+
+        return
     }
 
-    func deleteManualGlucose(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+    func deleteManualGlucose(withId id: String) async throws {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
         components.queryItems = [
-            URLQueryItem(name: "find[glucose][$exists]", value: "true"),
-            URLQueryItem(
-                name: "find[created_at][$eq]",
-                value: Formatter.iso8601withFractionalSeconds.string(from: date)
-            )
+            URLQueryItem(name: "find[id][$eq]", value: id)
         ]
 
-        var request = URLRequest(url: components.url!)
+        guard let url = components.url else {
+            throw URLError(.badURL)
+        }
+
+        var request = URLRequest(url: url)
         request.allowsConstrainedNetworkAccess = false
         request.timeoutInterval = Config.timeout
         request.httpMethod = "DELETE"
@@ -201,27 +193,30 @@ extension NightscoutAPI {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
 
-        return service.run(request)
-            .retry(Config.retryCount)
-            .map { _ in () }
-            .eraseToAnyPublisher()
+        let (_, response) = try await URLSession.shared.data(for: request)
+
+        guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
+            throw URLError(.badServerResponse)
+        }
+
+        debugPrint("Delete successful for ID \(id)")
     }
 
-    func deleteInsulin(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+    func deleteInsulin(withId id: String) async throws {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
         components.queryItems = [
-            URLQueryItem(name: "find[bolus][$exists]", value: "true"),
-            URLQueryItem(
-                name: "find[created_at][$eq]",
-                value: Formatter.iso8601withFractionalSeconds.string(from: date)
-            )
+            URLQueryItem(name: "find[id][$eq]", value: id)
         ]
 
-        var request = URLRequest(url: components.url!)
+        guard let url = components.url else {
+            throw URLError(.badURL)
+        }
+
+        var request = URLRequest(url: url)
         request.allowsConstrainedNetworkAccess = false
         request.timeoutInterval = Config.timeout
         request.httpMethod = "DELETE"
@@ -230,12 +225,42 @@ extension NightscoutAPI {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
 
-        return service.run(request)
-            .retry(Config.retryCount)
-            .map { _ in () }
-            .eraseToAnyPublisher()
+        let (_, response) = try await URLSession.shared.data(for: request)
+
+        guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
+            throw URLError(.badServerResponse)
+        }
     }
 
+//    func deleteInsulin(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+//        var components = URLComponents()
+//        components.scheme = url.scheme
+//        components.host = url.host
+//        components.port = url.port
+//        components.path = Config.treatmentsPath
+//        components.queryItems = [
+//            URLQueryItem(name: "find[bolus][$exists]", value: "true"),
+//            URLQueryItem(
+//                name: "find[created_at][$eq]",
+//                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+//            )
+//        ]
+//
+//        var request = URLRequest(url: components.url!)
+//        request.allowsConstrainedNetworkAccess = false
+//        request.timeoutInterval = Config.timeout
+//        request.httpMethod = "DELETE"
+//
+//        if let secret = secret {
+//            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+//        }
+//
+//        return service.run(request)
+//            .retry(Config.retryCount)
+//            .map { _ in () }
+//            .eraseToAnyPublisher()
+//    }
+
     func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme
@@ -250,7 +275,7 @@ extension NightscoutAPI {
             ),
             URLQueryItem(
                 name: "find[enteredBy][$ne]",
-                value: NigtscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
+                value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
             ),
             URLQueryItem(name: "find[duration][$exists]", value: "true")
         ]
@@ -315,14 +340,18 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
-    func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
+    func uploadTreatments(_ treatments: [NightscoutTreatment]) async throws {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
 
-        var request = URLRequest(url: components.url!)
+        guard let requestURL = components.url else {
+            throw URLError(.badURL)
+        }
+
+        var request = URLRequest(url: requestURL)
         request.allowsConstrainedNetworkAccess = false
         request.timeoutInterval = Config.timeout
         request.addValue("application/json", forHTTPHeaderField: "Content-Type")
@@ -330,16 +359,29 @@ extension NightscoutAPI {
         if let secret = secret {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
-        request.httpBody = try? JSONCoding.encoder.encode(treatments)
+
+        do {
+            let encodedBody = try JSONCoding.encoder.encode(treatments)
+            request.httpBody = encodedBody
+            debugPrint("Payload treatments size: \(encodedBody.count) bytes")
+            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+        } catch {
+            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            throw error
+        }
         request.httpMethod = "POST"
 
-        return service.run(request)
-            .retry(Config.retryCount)
-            .map { _ in () }
-            .eraseToAnyPublisher()
+        let (data, response) = try await URLSession.shared.data(for: request)
+
+        // Check the response status code
+        guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
+            throw URLError(.badServerResponse)
+        }
+
+        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
 
-    func uploadGlucose(_ glucose: [BloodGlucose]) -> AnyPublisher<Void, Swift.Error> {
+    func uploadGlucose(_ glucose: [BloodGlucose]) async throws {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
@@ -354,13 +396,25 @@ extension NightscoutAPI {
         if let secret = secret {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
-        request.httpBody = try! JSONCoding.encoder.encode(glucose)
+        do {
+            let encodedBody = try JSONCoding.encoder.encode(glucose)
+            request.httpBody = encodedBody
+            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
+            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+        } catch {
+            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            throw error
+        }
         request.httpMethod = "POST"
 
-        return service.run(request)
-            .retry(Config.retryCount)
-            .map { _ in () }
-            .eraseToAnyPublisher()
+        let (data, response) = try await URLSession.shared.data(for: request)
+
+        // Check the response status code
+        guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
+            throw URLError(.badServerResponse)
+        }
+
+        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
 
     func uploadStats(_ stats: NightscoutStatistics) -> AnyPublisher<Void, Swift.Error> {

+ 294 - 223
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -6,16 +6,16 @@ import Swinject
 import UIKit
 
 protocol NightscoutManager: GlucoseSource {
-    func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never>
+    func fetchGlucose(since date: Date) async -> [BloodGlucose]
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
     func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
-    func deleteCarbs(_ treatment: DataTable.Treatment, complexMeal: Bool)
-    func deleteInsulin(at date: Date)
-    func deleteManualGlucose(at: Date)
+    func deleteCarbs(withID id: String) async
+    func deleteInsulin(withID id: String) async
+    func deleteManualGlucose(withID id: String) async
     func uploadStatus()
-    func uploadGlucose()
-    func uploadManualGlucose()
+    func uploadGlucose() async
+    func uploadManualGlucose() async
     func uploadStatistics(dailystat: Statistics)
     func uploadPreferences(_ preferences: Preferences)
     func uploadProfileAndSettings(_: Bool)
@@ -38,6 +38,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var ping: TimeInterval?
 
+    private var backgroundContext = CoreDataStack.shared.newTaskContext()
+
     private var lifetime = Lifetime()
 
     private var isNetworkReachable: Bool {
@@ -72,10 +74,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func subscribe() {
-        broadcaster.register(PumpHistoryObserver.self, observer: self)
-        broadcaster.register(CarbsObserver.self, observer: self)
-        broadcaster.register(TempTargetsObserver.self, observer: self)
-        broadcaster.register(GlucoseObserver.self, observer: self)
+        setupNotification()
         _ = reachabilityManager.startListening(onQueue: processQueue) { status in
             debug(.nightscout, "Network status: \(status)")
         }
@@ -102,13 +101,13 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         return maybeNightscout?.url
     }
 
-    func fetchGlucose(since date: Date) -> AnyPublisher<[BloodGlucose], Never> {
+    func fetchGlucose(since date: Date) async -> [BloodGlucose] {
         let useLocal = settingsManager.settings.useLocalGlucoseSource
         ping = nil
 
         if !useLocal {
             guard isNetworkReachable else {
-                return Just([]).eraseToAnyPublisher()
+                return []
             }
         }
 
@@ -117,22 +116,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             : nightscoutAPI
 
         guard let nightscout = maybeNightscout else {
-            return Just([]).eraseToAnyPublisher()
+            return []
         }
 
         let startDate = Date()
 
-        return nightscout.fetchLastGlucose(sinceDate: date)
-            .tryCatch({ (error) -> AnyPublisher<[BloodGlucose], Error> in
-                print(error.localizedDescription)
-                return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
-            })
-            .replaceError(with: [])
-            .handleEvents(receiveOutput: { value in
-                guard value.isNotEmpty else { return }
-                self.ping = Date().timeIntervalSince(startDate)
-            })
-            .eraseToAnyPublisher()
+        do {
+            let glucose = try await nightscout.fetchLastGlucose(sinceDate: date)
+            if glucose.isNotEmpty {
+                ping = Date().timeIntervalSince(startDate)
+            }
+            return glucose
+        } catch {
+            print(error.localizedDescription)
+            return []
+        }
     }
 
     // MARK: - GlucoseSource
@@ -142,7 +140,13 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     var cgmType: CGMType = .nightscout
 
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        fetchGlucose(since: glucoseStorage.syncDate())
+        Future { promise in
+            Task {
+                let glucoseData = await self.fetchGlucose(since: self.glucoseStorage.syncDate())
+                promise(.success(glucoseData))
+            }
+        }
+        .eraseToAnyPublisher()
     }
 
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
@@ -182,133 +186,48 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .eraseToAnyPublisher()
     }
 
-    func deleteCarbs(_ treatment: DataTable.Treatment, complexMeal: Bool) {
-        guard let nightscout = nightscoutAPI, isUploadEnabled else {
-            carbsStorage.deleteCarbs(at: treatment.id, fpuID: treatment.fpuID ?? "", complex: complexMeal)
-            return
-        }
+    func deleteCarbs(withID id: String) async {
+        guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
 
-        print("meals 3: ID: " + (treatment.id ?? "").description + " FPU ID: " + (treatment.fpuID ?? "").description)
+        // TODO: - healthkit rewrite, deletion of FPUs
+//        healthkitManager.deleteCarbs(syncID: arg1, fpuID: arg2)
 
-        var arg1 = ""
-        var arg2 = ""
-        if complexMeal {
-            arg1 = treatment.id ?? ""
-            arg2 = treatment.fpuID ?? ""
-        } else if treatment.isFPU ?? false {
-            arg1 = ""
-            arg2 = treatment.fpuID ?? ""
-        } else {
-            arg1 = treatment.id
-            arg2 = ""
+        do {
+            try await nightscout.deleteCarbs(withId: id)
+            debug(.nightscout, "Carbs deleted")
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) Failed to delete Carbs from Nightscout with error: \(error.localizedDescription)"
+            )
         }
-        healthkitManager.deleteCarbs(syncID: arg1, fpuID: arg2)
+    }
 
-        if complexMeal {
-            nightscout.deleteCarbs(treatment)
-                .collect()
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: treatment.id ?? "", fpuID: treatment.fpuID ?? "", complex: true)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carbs deleted")
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: { _ in }
-                .store(in: &lifetime)
+    func deleteInsulin(withID id: String) async {
+        guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
 
-            if (treatment.fpuID ?? "") != "" {
-                nightscout.deleteCarbs(treatment)
-                    .collect()
-                    .sink { completion in
-                        switch completion {
-                        case .finished:
-                            debug(.nightscout, "Carb equivalents deleted from NS")
-                        case let .failure(error):
-                            info(
-                                .nightscout,
-                                "Deletion of carb equivalents in NightScout not done \n \(error.localizedDescription)",
-                                type: MessageType.warning
-                            )
-                        }
-                    } receiveValue: { _ in }
-                    .store(in: &lifetime)
-            }
-        } else if treatment.isFPU ?? false {
-            nightscout.deleteCarbs(treatment)
-                .collect()
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: "", fpuID: treatment.fpuID ?? "", complex: false)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carb equivalents deleted")
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carb equivalents in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: { _ in }
-                .store(in: &lifetime)
-        } else {
-            nightscout.deleteCarbs(treatment)
-                .collect()
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: treatment.id, fpuID: "", complex: false)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carbs deleted")
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: { _ in }
-                .store(in: &lifetime)
+        do {
+            try await nightscout.deleteInsulin(withId: id)
+            debug(.nightscout, "Insulin deleted")
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) Failed to delete Insulin from Nightscout with error: \(error.localizedDescription)"
+            )
         }
     }
 
-    func deleteInsulin(at date: Date) {
-        guard let nightscout = nightscoutAPI, isUploadEnabled else {
-            pumpHistoryStorage.deleteInsulin(at: date)
-            return
-        }
-
-        nightscout.deleteInsulin(at: date)
-            .sink { completion in
-                switch completion {
-                case .finished:
-                    self.pumpHistoryStorage.deleteInsulin(at: date)
-                    debug(.nightscout, "Carbs deleted")
-                case let .failure(error):
-                    debug(.nightscout, error.localizedDescription)
-                }
-            } receiveValue: {}
-            .store(in: &lifetime)
-    }
+    func deleteManualGlucose(withID id: String) async {
+        guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
 
-    func deleteManualGlucose(at date: Date) {
-        guard let nightscout = nightscoutAPI, isUploadEnabled else {
-            return
+        do {
+            try await nightscout.deleteManualGlucose(withId: id)
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error.localizedDescription)"
+            )
         }
-        nightscout.deleteManualGlucose(at: date)
-            .sink { completion in
-                switch completion {
-                case .finished:
-                    debug(.nightscout, "Manual Glucose entry deleted")
-                case let .failure(error):
-                    debug(.nightscout, error.localizedDescription)
-                }
-            } receiveValue: {}
-            .store(in: &lifetime)
     }
 
     func uploadStatistics(dailystat: Statistics) {
@@ -660,7 +579,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         var status: NightscoutStatus
 
         status = NightscoutStatus(
-            device: NigtscoutTreatment.local,
+            device: NightscoutTreatment.local,
             openaps: openapsStatus,
             pump: pump,
             uploader: uploader
@@ -685,15 +604,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 .store(in: &self.lifetime)
         }
 
-        uploadPodAge()
+        Task {
+            await uploadPodAge()
+        }
     }
 
-    func uploadPodAge() {
-        let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NigtscoutTreatment].self) ?? []
+    func uploadPodAge() async {
+        let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NightscoutTreatment].self) ?? []
         if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
            uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
         {
-            let siteTreatment = NigtscoutTreatment(
+            let siteTreatment = NightscoutTreatment(
                 duration: nil,
                 rawDuration: nil,
                 rawRate: nil,
@@ -701,7 +622,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 rate: nil,
                 eventType: .nsSiteChange,
                 createdAt: podAge,
-                enteredBy: NigtscoutTreatment.local,
+                enteredBy: NightscoutTreatment.local,
                 bolus: nil,
                 insulin: nil,
                 notes: nil,
@@ -711,7 +632,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 targetTop: nil,
                 targetBottom: nil
             )
-            uploadTreatments([siteTreatment], fileToSave: OpenAPS.Nightscout.uploadedPodAge)
+            await uploadTreatments([siteTreatment], fileToSave: OpenAPS.Nightscout.uploadedPodAge)
         }
     }
 
@@ -819,7 +740,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             startDate: now,
             mills: Int(now.timeIntervalSince1970) * 1000,
             units: nsUnits,
-            enteredBy: NigtscoutTreatment.local,
+            enteredBy: NightscoutTreatment.local,
             store: [defaultProfile: ps]
         )
 
@@ -863,114 +784,264 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadGlucose() {
-        uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
-        uploadTreatments(glucoseStorage.nightscoutCGMStateNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCGMState)
+    func uploadGlucose() async {
+        await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToNightscout())
+        await uploadTreatments(
+            glucoseStorage.getCGMStateNotYetUploadedToNightscout(),
+            fileToSave: OpenAPS.Nightscout.uploadedCGMState
+        )
     }
 
-    func uploadManualGlucose() {
-        uploadTreatments(
-            glucoseStorage.nightscoutManualGlucoseNotUploaded(),
-            fileToSave: OpenAPS.Nightscout.uploadedManualGlucose
-        )
+    func uploadManualGlucose() async {
+        await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
     }
 
-    private func uploadPumpHistory() {
-        uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
+    private func uploadPumpHistory() async {
+        await uploadTreatments(
+            pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout(),
+            fileToSave: OpenAPS.Nightscout.uploadedPumphistory
+        )
     }
 
-    private func uploadCarbs() {
-        uploadTreatments(carbsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedCarbs)
+    private func uploadCarbs() async {
+        await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
+        await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
     }
 
-    private func uploadTempTargets() {
-        uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
+    private func uploadTempTargets() async {
+        await uploadTreatments(
+            tempTargetsStorage.nightscoutTreatmentsNotUploaded(),
+            fileToSave: OpenAPS.Nightscout.uploadedTempTargets
+        )
     }
 
-    private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
+    private func uploadGlucose(_ glucose: [BloodGlucose]) async {
         guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
             return
         }
-        // check if unique code
-        // var uuid = UUID(uuidString: yourString) This will return nil if yourString is not a valid UUID
-        let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id) != nil }
 
-        processQueue.async {
-            glucoseWithoutCorrectID.chunks(ofCount: 100)
-                .map { chunk -> AnyPublisher<Void, Error> in
-                    nightscout.uploadGlucose(Array(chunk))
+        do {
+            // Upload in Batches of 100
+            for chunk in glucose.chunks(ofCount: 100) {
+                try await nightscout.uploadGlucose(Array(chunk))
+            }
+
+            // If successful, update the isUploadedToNS property of the GlucoseStored objects
+            await updateGlucoseAsUploaded(glucose)
+
+            debug(.nightscout, "Glucose uploaded")
+        } catch {
+            debug(.nightscout, "Upload of glucose failed: \(error.localizedDescription)")
+        }
+    }
+
+    private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
+        await backgroundContext.perform {
+            let ids = glucose.map(\.id) as NSArray
+            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
+            let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
+                for result in results {
+                    result.isUploadedToNS = true
                 }
-                .reduce(
-                    Just(()).setFailureType(to: Error.self)
-                        .eraseToAnyPublisher()
-                ) { (result, next) -> AnyPublisher<Void, Error> in
-                    Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    private func uploadTreatments(_ treatments: [NightscoutTreatment], fileToSave _: String) async {
+        guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
+            return
+        }
+
+        do {
+            for chunk in treatments.chunks(ofCount: 100) {
+                try await nightscout.uploadTreatments(Array(chunk))
+            }
+
+            // If successful, update the isUploadedToNS property of the PumpEventStored objects
+            await updateTreatmentsAsUploaded(treatments)
+
+            debug(.nightscout, "Treatments uploaded")
+        } catch {
+            debug(.nightscout, error.localizedDescription)
+        }
+    }
+
+    private func updateTreatmentsAsUploaded(_ treatments: [NightscoutTreatment]) async {
+        await backgroundContext.perform {
+            let ids = treatments.map(\.id) as NSArray
+            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
+            let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
+                for result in results {
+                    result.isUploadedToNS = true
                 }
-                .dropFirst()
-                .sink { completion in
-                    switch completion {
-                    case .finished:
-                        self.storage.save(glucose, as: fileToSave)
-                        debug(.nightscout, "Glucose uploaded")
-                    case let .failure(error):
-                        debug(.nightscout, "Upload of glucose failed: " + error.localizedDescription)
-                    }
-                } receiveValue: {}
-                .store(in: &self.lifetime)
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
+                )
+            }
         }
     }
 
-    private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
+    private func uploadManualGlucose(_ treatments: [NightscoutTreatment]) async {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
             return
         }
 
-        processQueue.async {
-            treatments.chunks(ofCount: 100)
-                .map { chunk -> AnyPublisher<Void, Error> in
-                    nightscout.uploadTreatments(Array(chunk))
+        do {
+            for chunk in treatments.chunks(ofCount: 100) {
+                try await nightscout.uploadTreatments(Array(chunk))
+            }
+
+            // If successful, update the isUploadedToNS property of the GlucoseStored objects
+            await updateManualGlucoseAsUploaded(treatments)
+
+            debug(.nightscout, "Treatments uploaded")
+        } catch {
+            debug(.nightscout, error.localizedDescription)
+        }
+    }
+
+    private func updateManualGlucoseAsUploaded(_ treatments: [NightscoutTreatment]) async {
+        await backgroundContext.perform {
+            let ids = treatments.map(\.id) as NSArray
+            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
+            let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
+                for result in results {
+                    result.isUploadedToNS = true
                 }
-                .reduce(
-                    Just(()).setFailureType(to: Error.self)
-                        .eraseToAnyPublisher()
-                ) { (result, next) -> AnyPublisher<Void, Error> in
-                    Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    private func uploadCarbs(_ treatments: [NightscoutTreatment]) async {
+        guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
+            return
+        }
+
+        do {
+            for chunk in treatments.chunks(ofCount: 100) {
+                try await nightscout.uploadTreatments(Array(chunk))
+            }
+
+            // If successful, update the isUploadedToNS property of the CarbEntryStored objects
+            await updateCarbsAsUploaded(treatments)
+
+            debug(.nightscout, "Treatments uploaded")
+        } catch {
+            debug(.nightscout, error.localizedDescription)
+        }
+    }
+
+    private func updateCarbsAsUploaded(_ treatments: [NightscoutTreatment]) async {
+        await backgroundContext.perform {
+            let ids = treatments.map(\.id) as NSArray
+            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
+            let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
+                for result in results {
+                    result.isUploadedToNS = true
                 }
-                .dropFirst()
-                .sink { completion in
-                    switch completion {
-                    case .finished:
-                        self.storage.save(treatments, as: fileToSave)
-                        debug(.nightscout, "Treatments uploaded")
-                    case let .failure(error):
-                        debug(.nightscout, error.localizedDescription)
-                    }
-                } receiveValue: {}
-                .store(in: &self.lifetime)
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
+                )
+            }
         }
     }
 }
 
-extension BaseNightscoutManager: PumpHistoryObserver {
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        uploadPumpHistory()
+extension Array {
+    func chunks(ofCount count: Int) -> [[Element]] {
+        stride(from: 0, to: self.count, by: count).map {
+            Array(self[$0 ..< Swift.min($0 + count, self.count)])
+        }
     }
 }
 
-extension BaseNightscoutManager: CarbsObserver {
-    func carbsDidUpdate(_: [CarbsEntry]) {
-        uploadCarbs()
+extension BaseNightscoutManager {
+    /// listens for the notifications sent when the managedObjectContext has saved!
+    func setupNotification() {
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(contextDidSave(_:)),
+            name: Notification.Name.NSManagedObjectContextDidSave,
+            object: nil
+        )
     }
-}
 
-extension BaseNightscoutManager: TempTargetsObserver {
-    func tempTargetsDidUpdate(_: [TempTarget]) {
-        uploadTempTargets()
+    /// determine the actions when the context has changed
+    ///
+    /// its done on a background thread and after that the UI gets updated on the main thread
+    @objc private func contextDidSave(_ notification: Notification) {
+        guard let userInfo = notification.userInfo else { return }
+
+        Task { [weak self] in
+            await self?.processUpdates(userInfo: userInfo)
+        }
     }
-}
 
-extension BaseNightscoutManager: GlucoseObserver {
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-        uploadManualGlucose()
+    private func processUpdates(userInfo: [AnyHashable: Any]) async {
+        var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
+        objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
+        objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+
+        let manualGlucoseUpdates = objects.filter { $0 is GlucoseStored }
+        let carbUpdates = objects.filter { $0 is CarbEntryStored }
+        let pumpHistoryUpdates = objects.filter { $0 is PumpEventStored }
+
+        if manualGlucoseUpdates.isNotEmpty {
+            Task.detached {
+                await self.uploadManualGlucose()
+            }
+        }
+        if carbUpdates.isNotEmpty {
+            Task.detached {
+                await self.uploadCarbs()
+            }
+        }
+        if pumpHistoryUpdates.isNotEmpty {
+            Task.detached {
+                await self.uploadPumpHistory()
+            }
+        }
     }
 }

+ 4 - 0
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -81,6 +81,10 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
     }
 
     public func updateUIView(_ textField: UITextField, context: Context) {
+        if textField.isFirstResponder {
+            return
+        }
+
         if text != 0 {
             let newText = numberFormatter.string(for: text) ?? ""
             if textField.text != newText {

+ 1 - 0
GlucoseStored+CoreDataProperties.swift

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

+ 13 - 1
LiveActivity/LiveActivity.swift

@@ -93,6 +93,18 @@ struct LiveActivity: Widget {
                     }
                 }
             })
+            VStack(alignment: .trailing, spacing: 1, content: {
+                if context.state.isOverrideActive {
+                    if !context.isStale {
+                        Image(systemName: "person.crop.circle.fill.badge.checkmark")
+                            .font(.title3)
+                    } else {
+                        Image(systemName: "person.crop.circle.fill.badge.checkmark")
+                            .font(.title3)
+                            .strikethrough(pattern: .solid, color: .red.opacity(0.6))
+                    }
+                }
+            })
         }
     }
 
@@ -282,7 +294,7 @@ struct LiveActivity: Widget {
                 .activityBackgroundTint(Color.clear)
             } else {
                 HStack(spacing: 12) {
-                    chart(context: context).frame(width: UIScreen.main.bounds.width / 1.8)
+                    chart(context: context).frame(maxWidth: UIScreen.main.bounds.width / 1.8)
                     VStack(alignment: .leading) {
                         Spacer()
                         bgLabel(context: context)

+ 4 - 0
MealPresetStored+CoreDataClass.swift

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

+ 15 - 0
MealPresetStored+CoreDataProperties.swift

@@ -0,0 +1,15 @@
+import CoreData
+import Foundation
+
+public extension MealPresetStored {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<MealPresetStored> {
+        NSFetchRequest<MealPresetStored>(entityName: "MealPresetStored")
+    }
+
+    @NSManaged var carbs: NSDecimalNumber?
+    @NSManaged var dish: String?
+    @NSManaged var fat: NSDecimalNumber?
+    @NSManaged var protein: NSDecimalNumber?
+}
+
+extension MealPresetStored: Identifiable {}

+ 2 - 2
Model/CoreDataStack.swift

@@ -158,11 +158,11 @@ class CoreDataStack: ObservableObject {
 extension CoreDataStack {
     /// Synchronously delete entry with specified object IDs
     ///  - Tag: synchronousDelete
-    func deleteObject(identifiedBy objectID: NSManagedObjectID) {
+    func deleteObject(identifiedBy objectID: NSManagedObjectID) async {
         let viewContext = persistentContainer.viewContext
         debugPrint("Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
 
-        viewContext.perform {
+        await viewContext.perform {
             do {
                 let entryToDelete = viewContext.object(with: objectID)
                 viewContext.delete(entryToDelete)

+ 29 - 9
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="22758" systemVersion="23E214" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -10,8 +10,10 @@
         <attribute name="carbs" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="fat" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+        <attribute name="fpuID" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="isFPU" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
@@ -42,12 +44,16 @@
         <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"/>
+        <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
         <fetchIndex name="byIsManual">
             <fetchIndexElement property="isManual" type="Binary" order="ascending"/>
         </fetchIndex>
+        <fetchIndex name="byIsUploadedToNS">
+            <fetchIndexElement property="isUploadedToNS" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     <entity name="ImportError" representedClassName="ImportError" syncable="YES">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@@ -60,6 +66,12 @@
         <attribute name="loopStatus" optional="YES" attributeType="String"/>
         <attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
     </entity>
+    <entity name="MealPresetStored" representedClassName="MealPresetStored" syncable="YES">
+        <attribute name="carbs" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="dish" optional="YES" attributeType="String"/>
+        <attribute name="fat" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="protein" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+    </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"/>
@@ -108,6 +120,16 @@
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
         </fetchIndex>
     </entity>
+    <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
+        <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="startDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="target" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <relationship name="override" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="OverrideStored" inverseName="overrideRun" inverseEntity="OverrideStored"/>
+        <fetchIndex name="byDate">
+            <fetchIndexElement property="startDate" type="Binary" order="ascending"/>
+        </fetchIndex>
+    </entity>
     <entity name="OverrideStored" representedClassName="OverrideStored" syncable="YES">
         <attribute name="advancedSettings" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="cr" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
@@ -121,6 +143,7 @@
         <attribute name="isfAndCr" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isPreset" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="name" optional="YES" attributeType="String"/>
+        <attribute name="orderPosition" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="percentage" optional="YES" attributeType="Double" defaultValueString="100" usesScalarValueType="YES"/>
         <attribute name="smbIsAlwaysOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="smbIsOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -128,6 +151,7 @@
         <attribute name="start" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="target" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="uamMinutes" optional="YES" attributeType="Decimal" defaultValueString="30"/>
+        <relationship name="overrideRun" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="OverrideRunStored" inverseName="override" inverseEntity="OverrideRunStored"/>
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
@@ -135,14 +159,10 @@
             <fetchIndexElement property="isPreset" type="Binary" order="ascending"/>
         </fetchIndex>
     </entity>
-    <entity name="Presets" representedClassName="Presets" syncable="YES">
-        <attribute name="carbs" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="dish" optional="YES" attributeType="String"/>
-        <attribute name="fat" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="protein" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-    </entity>
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
-        <attribute name="id_" optional="YES" attributeType="String"/>
+        <attribute name="id" optional="YES" attributeType="String"/>
+        <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="type" optional="YES" attributeType="String"/>
         <relationship name="bolus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BolusStored" inverseName="pumpEvent" inverseEntity="BolusStored"/>
@@ -152,7 +172,7 @@
         </fetchIndex>
         <uniquenessConstraints>
             <uniquenessConstraint>
-                <constraint value="id_"/>
+                <constraint value="id"/>
             </uniquenessConstraint>
         </uniquenessConstraints>
     </entity>

+ 20 - 0
Model/Helper/CarbEntryStored+helper.swift

@@ -11,6 +11,26 @@ extension NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(format: "isFPU == false AND date >= %@", date as NSDate)
     }
+
+    static var carbsNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@",
+            date as NSDate,
+            false as NSNumber,
+            false as NSNumber
+        )
+    }
+
+    static var fpusNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@",
+            date as NSDate,
+            false as NSNumber,
+            true as NSNumber
+        )
+    }
 }
 
 extension CarbEntryStored {

+ 18 - 4
Model/Helper/GlucoseStored+helper.swift

@@ -21,12 +21,11 @@ extension GlucoseStored {
     }
 
     static func glucoseIsFlat(_ glucose: [GlucoseStored]) -> Bool {
-        guard glucose.count >= 4 else { return false }
+        guard glucose.count >= 6 else { return false }
 
-        let lastThreeValues = glucose.suffix(4)
-        let firstValue = lastThreeValues.last?.glucose
+        let firstValue = glucose.first?.glucose
 
-        return lastThreeValues.allSatisfy { $0.glucose == firstValue }
+        return glucose.allSatisfy { $0.glucose == firstValue }
     }
 }
 
@@ -65,6 +64,21 @@ extension NSPredicate {
         let date = Date.oneWeekAgo
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
+
+    static var glucoseNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
+    }
+
+    static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToNS == %@ AND isManual == %@",
+            date as NSDate,
+            false as NSNumber,
+            true as NSNumber
+        )
+    }
 }
 
 extension GlucoseStored: Encodable {

+ 7 - 2
Model/Helper/PumpEvent+helper.swift

@@ -70,6 +70,11 @@ extension NSPredicate {
         let date20m = Date.twentyMinutesAgo
         return NSPredicate(format: "timestamp >= %@ && timestamp == %@", date20m as NSDate, date as NSDate)
     }
+
+    static var pumpEventsNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
+    }
 }
 
 // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
@@ -137,7 +142,7 @@ extension PumpEventStored {
         }
 
         let bolusDTO = BolusDTO(
-            id: id,
+            id: id ?? UUID().uuidString,
             timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
             amount: amount.doubleValue,
             isExternal: bolus.isExternal,
@@ -167,7 +172,7 @@ extension PumpEventStored {
         }
 
         let tempBasalDurationDTO = TempBasalDurationDTO(
-            id: id,
+            id: id ?? UUID().uuidString,
             timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
             duration: Int(tempBasal.duration)
         )

+ 4 - 0
OverrideRunStored+CoreDataClass.swift

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

+ 16 - 0
OverrideRunStored+CoreDataProperties.swift

@@ -0,0 +1,16 @@
+import CoreData
+import Foundation
+
+public extension OverrideRunStored {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<OverrideRunStored> {
+        NSFetchRequest<OverrideRunStored>(entityName: "OverrideRunStored")
+    }
+
+    @NSManaged var endDate: Date?
+    @NSManaged var startDate: Date?
+    @NSManaged var target: NSDecimalNumber?
+    @NSManaged var id: UUID?
+    @NSManaged var override: OverrideStored?
+}
+
+extension OverrideRunStored: Identifiable {}

+ 3 - 6
OverrideStored+CoreDataProperties.swift

@@ -25,11 +25,8 @@ public extension OverrideStored {
     @NSManaged var start: NSDecimalNumber?
     @NSManaged var target: NSDecimalNumber?
     @NSManaged var uamMinutes: NSDecimalNumber?
+    @NSManaged var orderPosition: Int16
+    @NSManaged var overrideRun: OverrideRunStored?
 }
 
-extension OverrideStored: Identifiable {
-    override public func awakeFromInsert() {
-        super.awakeFromInsert()
-        id = UUID().uuidString
-    }
-}
+extension OverrideStored: Identifiable {}

+ 0 - 4
Presets+CoreDataClass.swift

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

+ 0 - 15
Presets+CoreDataProperties.swift

@@ -1,15 +0,0 @@
-import CoreData
-import Foundation
-
-public extension Presets {
-    @nonobjc class func fetchRequest() -> NSFetchRequest<Presets> {
-        NSFetchRequest<Presets>(entityName: "Presets")
-    }
-
-    @NSManaged var carbs: NSDecimalNumber?
-    @NSManaged var dish: String?
-    @NSManaged var fat: NSDecimalNumber?
-    @NSManaged var protein: NSDecimalNumber?
-}
-
-extension Presets: Identifiable {}

+ 0 - 72
PumpEventStored+CoreDataClass.swift

@@ -7,76 +7,4 @@ import Foundation
     enum PumpEventErrorType: Int {
         case duplicate = 1001
     }
-
-    override public func awakeFromInsert() {
-        id_ = UUID().uuidString
-    }
-
-//    override public func validateForInsert() throws {
-//        try super.validateForInsert()
-//        try validateUniqueTimestamp()
-//    }
-//
-//    private func validateUniqueTimestamp() throws {
-//        guard let context = managedObjectContext, let timestamp = self.timestamp else {
-//            return
-//        }
-//
-//        let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
-//        fetchRequest.predicate = NSPredicate.duplicateInLastFourLoops(timestamp)
-//
-//        do {
-//            let results = try context.fetch(fetchRequest)
-//            if !results.isEmpty {
-//                print("Found duplicate PumpEventStored objects:")
-//                for result in results {
-//                    print("Timestamp: \(String(describing: result.timestamp))")
-//                }
-//                let error = NSError(domain: errorDomain, code: PumpEventErrorType.duplicate.rawValue, userInfo: [
-//                    NSLocalizedDescriptionKey: "There is already a PumpEventStored with the same timestamp within the last 20 minutes.",
-//                    "PumpEventErrorType": PumpEventErrorType.duplicate
-//                ])
-//                throw error
-//            }
-//        } catch {
-//            throw error
-//        }
-//    }
-
-//
-//    override public func validateValue(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey key: String) throws {
-//        try super.validateValue(value, forKey: key)
-//
-//        if key == "timestamp" {
-//            try validateUniqueTimestamp(value)
-//        }
-//    }
-//
-//    private func validateUniqueTimestamp(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
-//        guard
-//            let timestamp = value.pointee as? Date
-//        else {
-//            return
-//        }
-//
-//        let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
-//        fetchRequest.predicate = NSPredicate.duplicateInLastFourLoops(timestamp)
-//
-//        do {
-//            let results = try CoreDataStack.shared.backgroundContext.fetch(fetchRequest)
-//            if !results.isEmpty {
-//                print("Found duplicate PumpEventStored objects:")
-//                for result in results {
-//                    print("Timestamp: \(String(describing: result.timestamp))")
-//                }
-//                let error = NSError(domain: errorDomain, code: PumpEventErrorType.duplicate.rawValue, userInfo: [
-//                    NSLocalizedDescriptionKey: "There is already a PumpEventStored with the same timestamp within the last 4 loops.",
-//                    "PumpEventErrorType": PumpEventErrorType.duplicate
-//                ])
-//                throw error
-//            }
-//        } catch {
-//            throw error
-//        }
-//    }
 }

+ 3 - 7
PumpEventStored+CoreDataProperties.swift

@@ -6,17 +6,13 @@ public extension PumpEventStored {
         NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
     }
 
-    @NSManaged var id_: String!
+    @NSManaged var id: String?
     @NSManaged var timestamp: Date?
     @NSManaged var type: String?
+    @NSManaged var isUploadedToNS: Bool
+    @NSManaged var note: String?
     @NSManaged var bolus: BolusStored?
     @NSManaged var tempBasal: TempBasalStored?
 }
 
 extension PumpEventStored: Identifiable {}
-
-public extension PumpEventStored {
-    var id: String {
-        id_ ?? UUID().uuidString
-    }
-}