Przeglądaj źródła

Pull in latest 'Core-data-sync-trio'

polscm32 aka Marvout 1 rok temu
rodzic
commit
ec21e0b8e0
70 zmienionych plików z 3338 dodań i 1965 usunięć
  1. 64 12
      FreeAPS.xcodeproj/project.pbxproj
  2. 16 0
      FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  3. 1 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  4. 6 4
      FreeAPS/Sources/APS/APSManager.swift
  5. 13 34
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  6. 23 21
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  7. 88 12
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  8. 37 4
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  9. 165 5
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  10. 10 10
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  11. 112 3
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  12. 67 0
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  13. 3 2
      FreeAPS/Sources/Models/BloodGlucose.swift
  14. 1 1
      FreeAPS/Sources/Models/CarbsEntry.swift
  15. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  16. 22 0
      FreeAPS/Sources/Models/GlucoseColorScheme.swift
  17. 22 16
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  18. 109 98
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  19. 61 56
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  20. 7 2
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  21. 36 34
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  22. 120 45
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  23. 18 18
      FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  24. 16 14
      FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift
  25. 32 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  26. 60 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  27. 103 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  28. 58 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  29. 106 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  30. 38 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  31. 114 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  32. 81 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  33. 103 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  34. 76 596
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  35. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/CobChart.swift
  36. 26 4
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  37. 15 3
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  38. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/IobChart.swift
  39. 26 101
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  40. 50 36
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  41. 4 0
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  42. 7 2
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  43. 14 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  44. 3 1
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  45. 1 1
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  46. 29 5
      FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift
  47. 2 2
      FreeAPS/Sources/Modules/Stat/StatStateModel.swift
  48. 2 0
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  49. 1 0
      FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  50. 3 0
      FreeAPS/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  51. 37 1
      FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  52. 4 4
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  53. 500 487
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  54. 12 12
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  55. 3 3
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  56. 3 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  57. 3 0
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  58. 11 1
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  59. 474 245
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  60. 2 2
      FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift
  61. 31 26
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  62. 204 34
      LiveActivity/LiveActivity.swift
  63. 2 0
      Model/Classes+Properties/CarbEntryStored+CoreDataProperties.swift
  64. 2 0
      Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift
  65. 2 0
      Model/Classes+Properties/PumpEventStored+CoreDataProperties.swift
  66. 19 1
      Model/Helper/CarbEntryStored+helper.swift
  67. 30 0
      Model/Helper/GlucoseStored+helper.swift
  68. 10 0
      Model/Helper/PumpEvent+helper.swift
  69. 10 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  70. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 64 - 12
FreeAPS.xcodeproj/project.pbxproj

@@ -257,6 +257,14 @@
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
 		5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+helper.swift */; };
 		585E2CAE2BE7BF46006ECF1A /* PumpEvent+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E2CAD2BE7BF46006ECF1A /* PumpEvent+helper.swift */; };
+		58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */; };
+		58645B9B2CA2D24F008AFCE7 /* CarbSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */; };
+		58645B9D2CA2D275008AFCE7 /* DeterminationSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */; };
+		58645B9F2CA2D2BE008AFCE7 /* PumpHistorySetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */; };
+		58645BA12CA2D2F8008AFCE7 /* OverrideSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */; };
+		58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645BA22CA2D325008AFCE7 /* BatterySetup.swift */; };
+		58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645BA42CA2D347008AFCE7 /* ForecastSetup.swift */; };
+		58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58645BA62CA2D390008AFCE7 /* ChartAxisSetup.swift */; };
 		5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864E8582C42CFAE00294306 /* DeterminationStorage.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
@@ -324,8 +332,9 @@
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
+		BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
-		BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForeCastChart.swift */; };
+		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
@@ -448,6 +457,8 @@
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
+		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
+		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
@@ -912,6 +923,14 @@
 		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>"; };
 		585E2CAD2BE7BF46006ECF1A /* PumpEvent+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpEvent+helper.swift"; sourceTree = "<group>"; };
+		58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSetup.swift; sourceTree = "<group>"; };
+		58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbSetup.swift; sourceTree = "<group>"; };
+		58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationSetup.swift; sourceTree = "<group>"; };
+		58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistorySetup.swift; sourceTree = "<group>"; };
+		58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideSetup.swift; sourceTree = "<group>"; };
+		58645BA22CA2D325008AFCE7 /* BatterySetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatterySetup.swift; sourceTree = "<group>"; };
+		58645BA42CA2D347008AFCE7 /* ForecastSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastSetup.swift; sourceTree = "<group>"; };
+		58645BA62CA2D390008AFCE7 /* ChartAxisSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartAxisSetup.swift; sourceTree = "<group>"; };
 		5864E8582C42CFAE00294306 /* DeterminationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorage.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>"; };
@@ -980,8 +999,9 @@
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
+		BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetSetup.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
-		BDB899872C564509006F3298 /* ForeCastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeCastChart.swift; sourceTree = "<group>"; };
+		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
 		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
@@ -1106,6 +1126,8 @@
 		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
+		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
+		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
 		DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
@@ -1560,6 +1582,7 @@
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
+				58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */,
 				3811DE2C25C9D49500A708ED /* View */,
 			);
 			path = Home;
@@ -1901,6 +1924,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
@@ -1952,6 +1976,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
@@ -2254,6 +2279,22 @@
 			path = Helper;
 			sourceTree = "<group>";
 		};
+		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
+			isa = PBXGroup;
+			children = (
+				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
+				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
+				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
+				58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */,
+				58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */,
+				58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */,
+				58645BA22CA2D325008AFCE7 /* BatterySetup.swift */,
+				58645BA42CA2D347008AFCE7 /* ForecastSetup.swift */,
+				58645BA62CA2D390008AFCE7 /* ChartAxisSetup.swift */,
+			);
+			path = "HomeStateModel+Setup";
+			sourceTree = "<group>";
+		};
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			children = (
@@ -2356,7 +2397,7 @@
 			children = (
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
-				BDB899872C564509006F3298 /* ForeCastChart.swift */,
+				BDB899872C564509006F3298 /* ForecastChart.swift */,
 				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
 				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
 			);
@@ -3165,6 +3206,7 @@
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
+				58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
@@ -3191,6 +3233,7 @@
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
+				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
@@ -3245,6 +3288,7 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
+				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
@@ -3296,9 +3340,11 @@
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
+				58645B9F2CA2D2BE008AFCE7 /* PumpHistorySetup.swift in Sources */,
 				DD17454E2C55CA4D00211FAC /* UnitsLimitsSettingsDataFlow.swift in Sources */,
 				DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
+				BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				DD1745192C543B5700211FAC /* NotificationsView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
@@ -3306,6 +3352,7 @@
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
+				DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
@@ -3350,6 +3397,7 @@
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
+				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
@@ -3390,13 +3438,15 @@
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
-				BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */,
+				BDB899882C564509006F3298 /* ForecastChart.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
+				58645BA12CA2D2F8008AFCE7 /* OverrideSetup.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,
 				CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */,
 				BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
+				58645B9D2CA2D275008AFCE7 /* DeterminationSetup.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
@@ -3413,6 +3463,7 @@
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
 				CE7CA3522A064973004BE681 /* ListTempPresetsIntent.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
+				58645B9B2CA2D24F008AFCE7 /* CarbSetup.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				19A910362A24D6D700C8951B /* DateFilter.swift in Sources */,
 				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
@@ -3525,6 +3576,7 @@
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
+				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
@@ -3789,7 +3841,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
@@ -3849,7 +3901,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
@@ -3878,7 +3930,7 @@
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -3919,7 +3971,7 @@
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -4104,7 +4156,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -4125,7 +4177,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -4155,7 +4207,7 @@
 				INFOPLIST_FILE = LiveActivity/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -4189,7 +4241,7 @@
 				INFOPLIST_FILE = LiveActivity/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",

+ 16 - 0
FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -348,6 +348,8 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      enableThreadSanitizer = "YES"
+      enableUBSanitizer = "YES"
       launchStyle = "0"
       useCustomWorkingDirectory = "NO"
       ignoresPersistentStateOnLaunch = "NO"
@@ -364,6 +366,20 @@
             ReferencedContainer = "container:FreeAPS.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
+      <CommandLineArguments>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.ConcurrencyDebug 1"
+            isEnabled = "YES">
+         </CommandLineArgument>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.SQLDebug 1"
+            isEnabled = "NO">
+         </CommandLineArgument>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.MigrationDebug 1"
+            isEnabled = "NO">
+         </CommandLineArgument>
+      </CommandLineArguments>
       <EnvironmentVariables>
          <EnvironmentVariable
             key = "CG_NUMERICS_SHOW_BACKTRACE"

+ 1 - 0
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -37,6 +37,7 @@
   "high" : 180,
   "low" : 70,
   "hours" : 6,
+  "glucoseColorScheme" : "staticColor",
   "xGridLines" : true,
   "yGridLines" : true,
   "oneDimensionalGraph" : false,

+ 6 - 4
FreeAPS/Sources/APS/APSManager.swift

@@ -939,11 +939,13 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
         )
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return []
-        }
+        return await privateContext.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return []
+            }
 
-        return glucoseResults
+            return glucoseResults
+        }
     }
 
     // TODO: - Refactor this whole shit here...

+ 13 - 34
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -28,6 +28,7 @@ extension FetchGlucoseManager {
 
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
+
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var tidepoolService: TidepoolManager!
@@ -165,18 +166,16 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
     public func refreshCGM() {
         debug(.deviceManager, "refreshCGM by pump")
-        // updateGlucoseSource(cgmGlucoseSourceType: settingsManager.settings.cgm, cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier)
 
-        Publishers.CombineLatest3(
+        Publishers.CombineLatest(
             Just(glucoseStorage.syncDate()),
-            healthKitManager.fetch(nil),
             glucoseSource.fetchIfNeeded()
         )
         .eraseToAnyPublisher()
         .receive(on: processQueue)
-        .sink { syncDate, glucoseFromHealth, glucose in
+        .sink { syncDate, glucose in
             debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
-            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
+            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose)
         }
         .store(in: &lifetime)
     }
@@ -210,11 +209,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
     }
 
-    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
+    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) {
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
 
-        let allGlucose = newGlucose + glucoseFromHealth
         var filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
@@ -226,7 +224,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             backGroundFetchBGTaskID = .invalid
         }
 
-        guard allGlucose.isNotEmpty else {
+        guard newGlucose.isNotEmpty else {
             if let backgroundTask = backGroundFetchBGTaskID {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 backGroundFetchBGTaskID = .invalid
@@ -234,11 +232,11 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             return
         }
 
-        filteredByDate = allGlucose.filter { $0.dateString > syncDate }
+        filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
         guard filtered.isNotEmpty else {
-            // end of the BG tasks
+            // end of the Background tasks
             if let backgroundTask = backGroundFetchBGTaskID {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 backGroundFetchBGTaskID = .invalid
@@ -265,24 +263,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
         deviceDataManager.heartbeat(date: Date())
 
-        Task.detached {
-            await self.nightscoutManager.uploadGlucose()
-            await self.tidepoolService.uploadGlucose(device: self.cgmManager?.cgmManagerStatus.device)
-        }
-
-        let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
-
-        guard glucoseForHealth.isNotEmpty else {
-            // end of the BG tasks
-            if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundFetchBGTaskID = .invalid
-            }
-            return
-        }
-        healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
-
-        // end of the BG tasks
+        // End of the Background tasks
         if let backgroundTask = backGroundFetchBGTaskID {
             UIApplication.shared.endBackgroundTask(backgroundTask)
             backGroundFetchBGTaskID = .invalid
@@ -303,17 +284,15 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             }
             .sink { glucose in
                 debug(.nightscout, "FetchGlucoseManager callback sensor")
-                Publishers.CombineLatest3(
+                Publishers.CombineLatest(
                     Just(glucose),
-                    Just(self.glucoseStorage.syncDate()),
-                    self.healthKitManager.fetch(nil)
+                    Just(self.glucoseStorage.syncDate())
                 )
                 .eraseToAnyPublisher()
-                .sink { newGlucose, syncDate, glucoseFromHealth in
+                .sink { newGlucose, syncDate in
                     self.glucoseStoreAndHeartDecision(
                         syncDate: syncDate,
-                        glucose: newGlucose,
-                        glucoseFromHealth: glucoseFromHealth
+                        glucose: newGlucose
                     )
                 }
                 .store(in: &self.lifetime)

+ 23 - 21
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -102,17 +102,19 @@ final class OpenAPS {
             fetchLimit: 2
         )
 
-        guard let previousDeterminations = results as? [OrefDetermination] else {
-            return
-        }
+        await context.perform {
+            guard let previousDeterminations = results as? [OrefDetermination] else {
+                return
+            }
 
-        // We need to get the second last Determination for this comparison because we have saved the current Determination already to Core Data
-        if let previousDetermination = previousDeterminations.dropFirst().first {
-            let iobChanged = previousDetermination.iob != decimalToNSDecimalNumber(determination.iob)
-            let cobChanged = previousDetermination.cob != Int16(Int(determination.cob ?? 0))
+            // We need to get the second last Determination for this comparison because we have saved the current Determination already to Core Data
+            if let previousDetermination = previousDeterminations.dropFirst().first {
+                let iobChanged = previousDetermination.iob != self.decimalToNSDecimalNumber(determination.iob)
+                let cobChanged = previousDetermination.cob != Int16(Int(determination.cob ?? 0))
 
-            if iobChanged || cobChanged {
-                Foundation.NotificationCenter.default.post(name: .didUpdateCobIob, object: nil)
+                if iobChanged || cobChanged {
+                    Foundation.NotificationCenter.default.post(name: .didUpdateCobIob, object: nil)
+                }
             }
         }
     }
@@ -140,11 +142,11 @@ final class OpenAPS {
             batchSize: 24
         )
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return ""
-        }
-
         return await context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return ""
+            }
+
             // convert to JSON
             return self.jsonConverter.convertToJSON(glucoseResults)
         }
@@ -159,11 +161,11 @@ final class OpenAPS {
             ascending: false
         )
 
-        guard let carbResults = results as? [CarbEntryStored] else {
-            return ""
-        }
-
         let json = await context.perform {
+            guard let carbResults = results as? [CarbEntryStored] else {
+                return ""
+            }
+
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
 
             if let additionalCarbs = additionalCarbs {
@@ -209,11 +211,11 @@ final class OpenAPS {
             batchSize: 50
         )
 
-        guard let pumpEventResults = results as? [PumpEventStored] else {
-            return nil
-        }
-
         return await context.perform {
+            guard let pumpEventResults = results as? [PumpEventStored] else {
+                return nil
+            }
+
             return pumpEventResults.map(\.objectID)
         }
     }

+ 88 - 12
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -17,6 +17,8 @@ protocol CarbsStorage {
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
+    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
 }
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -44,8 +46,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
 
-        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+
+        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
 
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
@@ -63,6 +66,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
 
         // Extract dates into a set for efficient lookup
+        // Since we are not dealing with NSManagedObjects directly it is safe to pass properties between threads
         let existingTimestamps = Set(existing24hCarbEntries.compactMap { $0["date"] as? Date })
 
         // Remove all entries that have a matching date in existingTimestamps
@@ -115,7 +119,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      */
     private func processFPU(
-        entries _: [CarbsEntry],
+        entries: [CarbsEntry],
         fat: Decimal,
         protein: Decimal,
         createdAt: Date,
@@ -144,7 +148,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var numberOfEquivalents = carbEquivalents / carbEquivalentSize
 
         var useDate = actualDate ?? createdAt
-        let fpuID = UUID().uuidString
+        let fpuID = entries.first?.fpuID ?? UUID().uuidString
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
 
@@ -161,7 +165,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 fat: 0,
                 protein: 0,
                 note: nil,
-                enteredBy: CarbsEntry.manual, isFPU: true,
+                enteredBy: CarbsEntry.manual,
+                isFPU: true,
                 fpuID: fpuID
             )
             futureCarbArray.append(eachCarbEntry)
@@ -202,6 +207,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.id = UUID()
             newItem.isFPU = false
             newItem.isUploadedToNS = areFetchedFromRemote ? true : false
+            newItem.isUploadedToHealth = false
+            newItem.isUploadedToTidepool = false
+
+            if entry.fat != nil, entry.protein != nil, let fpuId = entry.fpuID {
+                newItem.fpuID = UUID(uuidString: fpuId)
+            }
 
             do {
                 guard self.coredataContext.hasChanges else { return }
@@ -213,8 +224,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
-        let commonFPUID =
-            UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
+        let commonFPUID = UUID(
+            uuidString: entries.first?.fpuID ?? UUID()
+                .uuidString
+        ) // 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(),
@@ -228,6 +241,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             carbEntry.fpuID = commonFPUID
             carbEntry.isFPU = true
             carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
+            // do NOT set Health and Tidepool flags to ensure they will NOT be uploaded
             return false // return false to continue
         }
         await coredataContext.perform {
@@ -337,11 +351,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
         )
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
-
         return await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                return []
+            }
+
             return carbEntries.map { result in
                 NightscoutTreatment(
                     duration: nil,
@@ -376,9 +390,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
         )
 
-        guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
-
         return await coredataContext.perform {
+            guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
+
             return fpuEntries.map { result in
                 NightscoutTreatment(
                     duration: nil,
@@ -402,4 +416,66 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             }
         }
     }
+
+    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToHealth,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: Decimal(result.fat),
+                    protein: Decimal(result.protein),
+                    note: result.note,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: result.isFPU,
+                    fpuID: result.fpuID?.uuidString
+                )
+            }
+        }
+    }
+
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: nil,
+                    protein: nil,
+                    note: result.note,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: nil,
+                    fpuID: nil
+                )
+            }
+        }
+    }
 }

+ 37 - 4
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -7,6 +7,10 @@ protocol DeterminationStorage {
     func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
+    func fetchForecastObjects(
+        for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
+        in context: NSManagedObjectContext
+    ) async -> (UUID, Forecast?, [ForecastValue])
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
 }
 
@@ -28,10 +32,10 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             fetchLimit: 1
         )
 
-        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
         return await backgroundContext.perform {
-            fetchedResults.map(\.objectID)
+            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
+            return fetchedResults.map(\.objectID)
         }
     }
 
@@ -39,7 +43,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         await context.perform {
             do {
                 guard let determination = try context.existingObject(with: determinationID) as? OrefDetermination,
-                      let forecastSet = determination.forecasts as? Set<NSManagedObject>
+                      let forecastSet = determination.forecasts
                 else {
                     return []
                 }
@@ -73,6 +77,35 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         }
     }
 
+    // Fetch forecast value IDs for a given data set
+    func fetchForecastObjects(
+        for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
+        in context: NSManagedObjectContext
+    ) async -> (UUID, Forecast?, [ForecastValue]) {
+        var forecast: Forecast?
+        var forecastValues: [ForecastValue] = []
+
+        do {
+            try await context.perform {
+                // Fetch the forecast object
+                forecast = try context.existingObject(with: data.forecastID) as? Forecast
+
+                // Fetch the first 3h of forecast values
+                for forecastValueID in data.forecastValueIDs.prefix(36) {
+                    if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
+                        forecastValues.append(forecastValue)
+                    }
+                }
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
+            )
+        }
+
+        return (data.id, forecast, forecastValues)
+    }
+
     // Convert NSDecimalNumber to Decimal
     func decimal(from nsDecimalNumber: NSDecimalNumber?) -> Decimal {
         nsDecimalNumber?.decimalValue ?? 0.0

+ 165 - 5
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -2,6 +2,7 @@ import AVFAudio
 import Combine
 import CoreData
 import Foundation
+import LoopKit
 import SwiftDate
 import SwiftUI
 import Swinject
@@ -17,7 +18,12 @@ protocol GlucoseStorage {
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
+    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
+    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
+    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
     var alarm: GlucoseAlarm? { get }
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -88,6 +94,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
+                        glucoseEntry.isUploadedToHealth = false /// the value is not uploaded to Health (yet)
+                        glucoseEntry.isUploadedToTidepool = false /// the value is not uploaded to Tidepool (yet)
                         return false // Continue processing
                     }
                 )
@@ -241,7 +249,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     // Fetch glucose that is not uploaded to Nightscout yet
-    /// Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
+    /// - 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,
@@ -252,9 +260,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             fetchLimit: 288
         )
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
         return await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
             return fetchedResults.map { result in
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -272,7 +280,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
-    /// Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
+    /// - 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,
@@ -295,7 +303,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     rate: nil,
                     eventType: .capillaryGlucose,
                     createdAt: result.date,
-                    enteredBy: "Trio",
+                    enteredBy: CarbsEntry.manual,
                     bolus: nil,
                     insulin: nil,
                     notes: "Trio User",
@@ -326,6 +334,158 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
     }
 
+    // Fetch glucose that is not uploaded to Nightscout yet
+    /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
+    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.glucoseNotYetUploadedToHealth,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.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,
+                    glucose: Int(result.glucose)
+                )
+            }
+        }
+    }
+
+    // Fetch manual glucose that is not uploaded to Nightscout yet
+    /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
+    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.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,
+                    glucose: Int(result.glucose)
+                )
+            }
+        }
+    }
+
+    // Fetch glucose that is not uploaded to Tidepool yet
+    /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
+    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.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,
+                    glucose: Int(result.glucose)
+                )
+            }
+            .map { $0.convertStoredGlucoseSample(isManualGlucose: false) }
+        }
+    }
+
+    // Fetch manual glucose that is not uploaded to Tidepool yet
+    /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
+    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.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,
+                    glucose: Int(result.glucose)
+                )
+            }.map { $0.convertStoredGlucoseSample(isManualGlucose: true) }
+        }
+    }
+
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteGlucose"
+
+        await taskContext.perform {
+            do {
+                let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
+
+                guard let glucoseToDelete = result else {
+                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
+                    return
+                }
+
+                taskContext.delete(glucoseToDelete)
+
+                guard taskContext.hasChanges else { return }
+                try taskContext.save()
+                debugPrint("\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
+            } catch {
+                debugPrint(
+                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
+                )
+            }
+        }
+    }
+
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         coredataContext.performAndWait {

+ 10 - 10
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -45,9 +45,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             fetchLimit: 1
         )
 
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
         }
     }
@@ -62,9 +62,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             fetchLimit: fetchLimit
         )
 
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
         }
     }
@@ -79,9 +79,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: true
         )
 
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
         }
     }
@@ -220,9 +220,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: false
         )
 
-        guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
+
             return fetchedOverrides.map { override in
                 let duration = override.indefinite ? 1440 : override.duration ?? 0 // 1440 min = 1 day
                 return NightscoutExercise(
@@ -250,9 +250,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: false
         )
 
-        guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
+
             return fetchedOverrideRuns.map { overrideRun in
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
                 durationInMinutes = durationInMinutes < 1 ? 1 : durationInMinutes

+ 112 - 3
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -15,6 +15,8 @@ protocol PumpHistoryStorage {
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func recent() -> [PumpHistoryEvent]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
     func deleteInsulin(at date: Date)
 }
 
@@ -80,6 +82,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                                         existingEvent.bolus?.amount = amount as NSDecimalNumber
                                         existingEvent.bolus?.isSMB = dose.automatic ?? true
                                         existingEvent.isUploadedToNS = false
+                                        existingEvent.isUploadedToHealth = false
+                                        existingEvent.isUploadedToTidepool = false
 
                                         print("Updated existing event with smaller value: \(amount)")
                                     }
@@ -93,6 +97,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                         let newBolusEntry = BolusStored(context: self.context)
                         newBolusEntry.pumpEvent = newPumpEvent
@@ -122,6 +128,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = date
                         newPumpEvent.type = PumpEvent.tempBasal.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                         let newTempBasal = TempBasalStored(context: self.context)
                         newTempBasal.pumpEvent = newPumpEvent
@@ -140,6 +148,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                     case .resume:
                         guard existingEvents.isEmpty else {
@@ -152,6 +162,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpResume.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                     case .rewind:
                         guard existingEvents.isEmpty else {
@@ -164,6 +176,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.rewind.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                     case .prime:
                         guard existingEvents.isEmpty else {
@@ -176,6 +190,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.prime.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
                     case .alarm:
                         guard existingEvents.isEmpty else {
@@ -188,6 +204,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
                         newPumpEvent.note = event.title
 
                     default:
@@ -217,6 +235,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             newPumpEvent.timestamp = timestamp
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.isUploadedToNS = false
+            newPumpEvent.isUploadedToHealth = false
+            newPumpEvent.isUploadedToTidepool = false
 
             // create bolus entry and specify relationship to pump event
             let newBolusEntry = BolusStored(context: self.context)
@@ -274,10 +294,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             fetchLimit: 288
         )
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
-
         return await context.perform { [self] in
-            fetchedPumpEvents.map { event in
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+            return 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)
@@ -423,4 +443,93 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             }.compactMap { $0 }
         }
     }
+
+    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+        return await context.perform {
+            fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEvent.bolus.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .bolus,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.bolus?.amount as Decimal?
+                    )
+                case PumpEvent.tempBasal.rawValue:
+                    if let id = event.id, let timestamp = event.timestamp, let tempBasal = event.tempBasal,
+                       let tempBasalRate = tempBasal.rate
+                    {
+                        return PumpHistoryEvent(
+                            id: id,
+                            type: .tempBasal,
+                            timestamp: timestamp,
+                            amount: tempBasalRate as Decimal,
+                            duration: Int(tempBasal.duration)
+                        )
+                    } else {
+                        return nil
+                    }
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
+        }
+    }
+
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+        return await context.perform {
+            fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEvent.bolus.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .bolus,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.bolus?.amount as Decimal?,
+                        isSMB: event.bolus?.isSMB ?? true,
+                        isExternal: event.bolus?.isExternal ?? false
+                    )
+                case PumpEvent.tempBasal.rawValue:
+                    if let id = event.id, let timestamp = event.timestamp, let tempBasal = event.tempBasal,
+                       let tempBasalRate = tempBasal.rate
+                    {
+                        return PumpHistoryEvent(
+                            id: id,
+                            type: .tempBasal,
+                            timestamp: timestamp,
+                            amount: tempBasalRate as Decimal,
+                            duration: Int(tempBasal.duration)
+                        )
+                    } else {
+                        return nil
+                    }
+
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
+        }
+    }
 }

+ 67 - 0
FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift

@@ -0,0 +1,67 @@
+import Foundation
+import SwiftUI
+
+// Helper function to decide how to pick the glucose color
+public func getDynamicGlucoseColor(
+    glucoseValue: Decimal,
+    highGlucoseColorValue: Decimal,
+    lowGlucoseColorValue: Decimal,
+    targetGlucose: Decimal,
+    glucoseColorScheme: GlucoseColorScheme,
+    offset: Decimal
+) -> Color {
+    // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
+    if glucoseColorScheme == .dynamicColor {
+        return calculateHueBasedGlucoseColor(
+            glucoseValue: glucoseValue,
+            highGlucose: highGlucoseColorValue + (offset * 1.75),
+            lowGlucose: lowGlucoseColorValue - offset,
+            targetGlucose: targetGlucose
+        )
+    }
+    // Otheriwse, use static (orange = high, red = low, green = range)
+    else {
+        if glucoseValue >= highGlucoseColorValue {
+            return Color.orange
+        } else if glucoseValue <= lowGlucoseColorValue {
+            return Color.red
+        } else {
+            return Color.green
+        }
+    }
+}
+
+// Dynamic color - Define the hue values for the key points
+// We'll shift color gradually one glucose point at a time
+// We'll shift through the rainbow colors of ROY-G-BIV from low to high
+// Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
+public func calculateHueBasedGlucoseColor(
+    glucoseValue: Decimal,
+    highGlucose: Decimal,
+    lowGlucose: Decimal,
+    targetGlucose: Decimal
+) -> Color {
+    let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
+    let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
+    let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
+
+    // Calculate the hue based on the bgLevel
+    var hue: CGFloat
+    if glucoseValue <= lowGlucose {
+        hue = redHue
+    } else if glucoseValue >= highGlucose {
+        hue = purpleHue
+    } else if glucoseValue <= targetGlucose {
+        // Interpolate between red and green
+        let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
+
+        hue = redHue + ratio * (greenHue - redHue)
+    } else {
+        // Interpolate between green and purple
+        let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
+        hue = greenHue + ratio * (purpleHue - greenHue)
+    }
+    // Return the color with full saturation and brightness
+    let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
+    return color
+}

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

@@ -157,12 +157,13 @@ extension BloodGlucose: SavitzkyGolaySmoothable {
 }
 
 extension BloodGlucose {
-    func convertStoredGlucoseSample(device: HKDevice?) -> StoredGlucoseSample {
+    func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
         StoredGlucoseSample(
             syncIdentifier: id,
             startDate: dateString.date,
             quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
-            device: device
+            wasUserEntered: isManualGlucose,
+            device: HKDevice.local()
         )
     }
 }

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

@@ -49,7 +49,7 @@ extension CarbsEntry {
             grams: Double(carbs),
             startDate: createdAt,
             uuid: UUID(uuidString: id!),
-            provenanceIdentifier: enteredBy ?? "",
+            provenanceIdentifier: enteredBy ?? "Trio",
             syncIdentifier: id,
             syncVersion: nil,
             userCreatedDate: nil,

+ 5 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -53,6 +53,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var high: Decimal = 180
     var low: Decimal = 70
     var hours: Int = 6
+    var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var oneDimensionalGraph: Bool = false
@@ -250,6 +251,10 @@ extension FreeAPSSettings: Decodable {
             settings.hours = hours
         }
 
+        if let glucoseColorScheme = try? container.decode(GlucoseColorScheme.self, forKey: .glucoseColorScheme) {
+            settings.glucoseColorScheme = glucoseColorScheme
+        }
+
         if let xGridLines = try? container.decode(Bool.self, forKey: .xGridLines) {
             settings.xGridLines = xGridLines
         }

+ 22 - 0
FreeAPS/Sources/Models/GlucoseColorScheme.swift

@@ -0,0 +1,22 @@
+//
+//  GlucoseColorScheme.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 27.09.24.
+//
+import Foundation
+
+public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    public var id: String { rawValue }
+    case staticColor
+    case dynamicColor
+
+    var displayName: String {
+        switch self {
+        case .staticColor:
+            return "Static"
+        case .dynamicColor:
+            return "Dynamic"
+        }
+    }
+}

+ 22 - 16
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -20,6 +20,7 @@ extension Bolus {
 
         @Published var lowGlucose: Decimal = 70
         @Published var highGlucose: Decimal = 180
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
 
         @Published var predictions: Predictions?
         @Published var amount: Decimal = 0
@@ -116,7 +117,8 @@ extension Bolus {
         let now = Date.now
 
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-        let backgroundContext = CoreDataStack.shared.newTaskContext()
+        let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
+        let determinationFetchContext = CoreDataStack.shared.newTaskContext()
 
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var subscriptions = Set<AnyCancellable>()
@@ -129,6 +131,8 @@ extension Bolus {
                     .receive(on: DispatchQueue.global(qos: .background))
                     .share()
                     .eraseToAnyPublisher()
+            registerHandlers()
+            registerSubscribers()
             setupBolusStateConcurrently()
         }
 
@@ -136,12 +140,6 @@ extension Bolus {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
-                        self.registerHandlers()
-                    }
-                    group.addTask {
-                        self.registerSubscribers()
-                    }
-                    group.addTask {
                         self.setupGlucoseArray()
                     }
                     group.addTask {
@@ -237,6 +235,7 @@ extension Bolus {
             maxProtein = settings.settings.maxProtein
             useFPUconversion = settingsManager.settings.useFPUconversion
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         }
 
         private func getCurrentSettingValue(for type: SettingType) async {
@@ -393,9 +392,16 @@ extension Bolus {
 
                 await saveMeal()
 
-                // if glucose data is stale end the custom loading animation by hiding the modal
-                guard glucoseStorage.isGlucoseDataFresh(glucoseFromPersistence.first?.date) else {
-                    waitForSuggestion = false
+                // If glucose data is stale end the custom loading animation by hiding the modal
+                // Get date on Main thread
+                let date = await MainActor.run {
+                    glucoseFromPersistence.first?.date
+                }
+
+                guard glucoseStorage.isGlucoseDataFresh(date) else {
+                    await MainActor.run {
+                        waitForSuggestion = false
+                    }
                     return hideModal()
                 }
             }
@@ -613,16 +619,16 @@ extension Bolus.StateModel {
     private func fetchGlucose() async -> [NSManagedObjectID] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             key: "date",
             ascending: false,
             fetchLimit: 288
         )
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
         }
     }
@@ -658,16 +664,16 @@ extension Bolus.StateModel {
 
     private func mapForecastsForChart() async -> Determination? {
         let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationObjectIDs, context: backgroundContext)
+            .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
 
-        return await backgroundContext.perform {
+        return await determinationFetchContext.perform {
             guard let determinationObject = determinationObjects.first else {
                 return nil
             }
 
             let eventualBG = determinationObject.eventualBG?.intValue
 
-            let forecastsSet = determinationObject.forecasts as? Set<Forecast> ?? []
+            let forecastsSet = determinationObject.forecasts ?? []
             let predictions = Predictions(
                 iob: forecastsSet.extractValues(for: "iob"),
                 zt: forecastsSet.extractValues(for: "zt"),

+ 109 - 98
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -94,36 +94,40 @@ extension Bolus {
 
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
-                Text("Fat").foregroundColor(.orange)
-                Spacer()
-                TextFieldWithToolBar(
-                    text: $state.fat,
-                    placeholder: "0",
-                    keyboardType: .numberPad,
-                    numberFormatter: mealFormatter,
-                    previousTextField: { focusOnPreviousTextField(index: 2) },
-                    nextTextField: { focusOnNextTextField(index: 2) }
-                ).focused($focusedField, equals: .fat)
-                Text("g").foregroundColor(.secondary)
-            }
-            HStack {
-                Text("Protein").foregroundColor(.red)
-                Spacer()
-                TextFieldWithToolBar(
-                    text: $state.protein,
-                    placeholder: "0",
-                    keyboardType: .numberPad,
-                    numberFormatter: mealFormatter,
-                    previousTextField: { focusOnPreviousTextField(index: 3) },
-                    nextTextField: { focusOnNextTextField(index: 3) }
-                ).focused($focusedField, equals: .protein)
-                Text("g").foregroundColor(.secondary)
+                HStack {
+                    Text("Fat")
+                    TextFieldWithToolBar(
+                        text: $state.fat,
+                        placeholder: "0",
+                        keyboardType: .numberPad,
+                        numberFormatter: mealFormatter,
+                        previousTextField: { focusOnPreviousTextField(index: 2) },
+                        nextTextField: { focusOnNextTextField(index: 2) }
+                    ).focused($focusedField, equals: .fat)
+                    Text("g").foregroundColor(.secondary)
+                }
+
+                Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
+
+                HStack {
+                    Text("Protein")
+
+                    TextFieldWithToolBar(
+                        text: $state.protein,
+                        placeholder: "0",
+                        keyboardType: .numberPad,
+                        numberFormatter: mealFormatter,
+                        previousTextField: { focusOnPreviousTextField(index: 3) },
+                        nextTextField: { focusOnNextTextField(index: 3) }
+                    ).focused($focusedField, equals: .protein)
+                    Text("g").foregroundColor(.secondary)
+                }
             }
         }
 
         @ViewBuilder private func carbsTextField() -> some View {
             HStack {
-                Text("Carbs").fontWeight(.semibold)
+                Text("Carbs")
                 Spacer()
                 TextFieldWithToolBar(
                     text: $state.carbs,
@@ -133,7 +137,7 @@ extension Bolus {
                     previousTextField: { focusOnPreviousTextField(index: 1) },
                     nextTextField: { focusOnNextTextField(index: 1) }
                 ).focused($focusedField, equals: .carbs)
-                    .onChange(of: state.carbs) { _ in
+                    .onChange(of: state.carbs) {
                         handleDebouncedInput()
                     }
                 Text("g").foregroundColor(.secondary)
@@ -169,98 +173,100 @@ extension Bolus {
         var body: some View {
             ZStack(alignment: .center) {
                 VStack {
-                    Form {
+                    List {
                         Section {
-                            ForeCastChart(state: state, units: $state.units)
+                            ForecastChart(state: state, units: $state.units)
                                 .padding(.vertical)
                         }.listRowBackground(Color.chart)
 
                         Section {
                             carbsTextField()
 
-                            DisclosureGroup("Extras") {
-                                if state.useFPUconversion {
-                                    proteinAndFat()
-                                }
+                            if state.useFPUconversion {
+                                proteinAndFat()
+                            }
 
-                                // Time
+                            // Time
+                            HStack {
+                                // Semi-hacky workaround to make sure the List renders the horizontal divider properly between the `Time` and `Note` rows within the Section
                                 HStack {
-                                    Text("Time").foregroundStyle(Color.secondary)
-                                    Spacer()
-                                    if !pushed {
-                                        Button {
-                                            pushed = true
-                                        } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
-                                            .padding(.trailing, 5)
-                                    } else {
-                                        Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
-                                        label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
-                                        DatePicker(
-                                            "Time",
-                                            selection: $state.date,
-                                            displayedComponents: [.hourAndMinute]
-                                        ).controlSize(.mini)
-                                            .labelsHidden()
-                                        Button {
-                                            state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
-                                        }
-                                        label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
-                                    }
+                                    Text("")
+                                    Image(systemName: "clock").padding(.leading, -7)
                                 }
 
-                                // Notes
-                                HStack {
-                                    Image(systemName: "square.and.pencil").foregroundColor(.secondary)
-                                    TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+                                Spacer()
+                                if !pushed {
+                                    Button {
+                                        pushed = true
+                                    } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
+                                        .padding(.trailing, 5)
+                                } else {
+                                    Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
+                                    label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+
+                                    DatePicker(
+                                        "Time",
+                                        selection: $state.date,
+                                        displayedComponents: [.hourAndMinute]
+                                    ).controlSize(.mini)
+                                        .labelsHidden()
+                                    Button {
+                                        state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                    }
+                                    label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                 }
                             }
+
+                            // Notes
+                            HStack {
+                                Image(systemName: "square.and.pencil")
+                                TextFieldWithToolBarString(text: $state.note, placeholder: "Note...", maxLength: 25)
+                            }
                         }.listRowBackground(Color.chart)
 
                         Section {
-                            HStack {
-                                Button(action: {
-                                    state.showInfo.toggle()
-                                }, label: {
-                                    Image(systemName: "info.circle")
-                                    Text("Calculations")
-                                })
-                                    .foregroundStyle(.blue)
-                                    .font(.footnote)
-                                    .buttonStyle(PlainButtonStyle())
-                                    .frame(maxWidth: .infinity, alignment: .leading)
-
-                                if state.fattyMeals {
-                                    Spacer()
-                                    Toggle(isOn: $state.useFattyMealCorrectionFactor) {
-                                        Text("Fatty Meal")
-                                    }
-                                    .toggleStyle(CheckboxToggleStyle())
-                                    .font(.footnote)
-                                    .onChange(of: state.useFattyMealCorrectionFactor) { _ in
-                                        state.insulinCalculated = state.calculateInsulin()
-                                        if state.useFattyMealCorrectionFactor {
-                                            state.useSuperBolus = false
+                            if state.fattyMeals || state.sweetMeals {
+                                HStack(spacing: 10) {
+                                    if state.fattyMeals {
+                                        Toggle(isOn: $state.useFattyMealCorrectionFactor) {
+                                            Text("Fatty Meal")
+                                        }
+                                        .toggleStyle(CheckboxToggleStyle())
+                                        .font(.footnote)
+                                        .onChange(of: state.useFattyMealCorrectionFactor) {
+                                            state.insulinCalculated = state.calculateInsulin()
+                                            if state.useFattyMealCorrectionFactor {
+                                                state.useSuperBolus = false
+                                            }
                                         }
                                     }
-                                }
-                                if state.sweetMeals {
-                                    Spacer()
-                                    Toggle(isOn: $state.useSuperBolus) {
-                                        Text("Super Bolus")
-                                    }
-                                    .toggleStyle(CheckboxToggleStyle())
-                                    .font(.footnote)
-                                    .onChange(of: state.useSuperBolus) { _ in
-                                        state.insulinCalculated = state.calculateInsulin()
-                                        if state.useSuperBolus {
-                                            state.useFattyMealCorrectionFactor = false
+                                    if state.sweetMeals {
+                                        Toggle(isOn: $state.useSuperBolus) {
+                                            Text("Super Bolus")
+                                        }
+                                        .toggleStyle(CheckboxToggleStyle())
+                                        .font(.footnote)
+                                        .onChange(of: state.useSuperBolus) {
+                                            state.insulinCalculated = state.calculateInsulin()
+                                            if state.useSuperBolus {
+                                                state.useFattyMealCorrectionFactor = false
+                                            }
                                         }
                                     }
                                 }
                             }
 
                             HStack {
-                                Text("Recommended Bolus")
+                                HStack {
+                                    Text("Recommendation")
+                                    Button(action: {
+                                        state.showInfo.toggle()
+                                    }, label: {
+                                        Image(systemName: "info.circle")
+                                    })
+                                        .foregroundStyle(.blue)
+                                        .buttonStyle(PlainButtonStyle())
+                                }
                                 Spacer()
                                 Text(
                                     formatter
@@ -287,7 +293,7 @@ extension Bolus {
                                     previousTextField: { focusOnPreviousTextField(index: 4) },
                                     nextTextField: { focusOnNextTextField(index: 4) }
                                 ).focused($focusedField, equals: .bolus)
-                                    .onChange(of: state.amount) { _ in
+                                    .onChange(of: state.amount) {
                                         Task {
                                             await state.updateForecasts()
                                         }
@@ -296,14 +302,14 @@ extension Bolus {
                             }
 
                             HStack {
-                                Text("External insulin")
+                                Text("External Insulin")
                                 Spacer()
                                 Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
                             }
                         }.listRowBackground(Color.chart)
 
                         treatmentButton
-                    }
+                    }.listSectionSpacing(20)
                 }
                 .blur(radius: state.waitForSuggestion ? 5 : 0)
 
@@ -311,6 +317,8 @@ extension Bolus {
                     CustomProgressView(text: progressText.rawValue)
                 }
             }
+            .padding(.top)
+            .ignoresSafeArea(edges: .top)
             .scrollContentBackground(.hidden).background(color)
             .blur(radius: state.showInfo ? 3 : 0)
             .navigationTitle("Treatments")
@@ -380,7 +388,10 @@ extension Bolus {
                     .frame(height: 35)
             }
             .disabled(disableTaskButton)
-            .listRowBackground(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
+            .listRowBackground(
+                limitExceeded ? Color(.systemRed) :
+                    Color(.systemBlue)
+            )
             .shadow(radius: 3)
             .clipShape(RoundedRectangle(cornerRadius: 8))
         }

+ 61 - 56
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -3,7 +3,7 @@ import CoreData
 import Foundation
 import SwiftUI
 
-struct ForeCastChart: View {
+struct ForecastChart: View {
     @StateObject var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Binding var units: GlucoseUnits
@@ -35,69 +35,63 @@ struct ForeCastChart: View {
 
     var body: some View {
         VStack {
-            HStack {
-                HStack {
-                    Text("Added carbs: ")
-                        .font(.footnote)
-                        .fontWeight(.bold)
-                        .foregroundStyle(.orange)
-
-                    Text("\(state.carbs.description) g")
-                        .font(.footnote)
-                        .foregroundStyle(.orange)
-                }
-                .padding(8)
-                .background {
-                    RoundedRectangle(cornerRadius: 10)
-                        .fill(Color.orange.opacity(0.2))
-                }
+            forecastChartLabels
+                .padding(.bottom, 8)
 
-                Spacer()
+            forecastChart
+        }
+    }
 
-                HStack {
-                    Text("Added insulin: ")
-                        .font(.footnote)
-                        .fontWeight(.bold)
-                        .foregroundStyle(.blue)
+    private var forecastChartLabels: some View {
+        HStack {
+            HStack {
+                Image(systemName: "fork.knife")
+                Text("\(state.carbs.description) g")
+            }
+            .font(.footnote)
+            .foregroundStyle(.orange)
+            .padding(8)
+            .background {
+                RoundedRectangle(cornerRadius: 10)
+                    .fill(Color.orange.opacity(0.2))
+            }
 
-                    Text("\(state.amount.description) U")
-                        .font(.footnote)
-                        .foregroundStyle(.blue)
-                }
-                .padding(8)
-                .background {
-                    RoundedRectangle(cornerRadius: 10)
-                        .fill(Color.blue.opacity(0.2))
-                }
+            Spacer()
+
+            HStack {
+                Image(systemName: "syringe.fill")
+                Text("\(state.amount.description) U")
+            }
+            .font(.footnote)
+            .foregroundStyle(.blue)
+            .padding(8)
+            .background {
+                RoundedRectangle(cornerRadius: 10)
+                    .fill(Color.blue.opacity(0.2))
             }
 
-            forecastChart
-                .padding(.vertical, 3)
+            Spacer()
+
             HStack {
-                Spacer()
                 Image(systemName: "arrow.right.circle")
-                    .font(.system(size: 16, weight: .bold))
 
-                if let eventualBG = state.simulatedDetermination?.eventualBG {
+                if let simulatedDetermination = state.simulatedDetermination, let eventualBG = simulatedDetermination.eventualBG {
                     HStack {
                         Text(
-                            units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
+                            (units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL) + units.rawValue
                         )
-                        .font(.footnote)
-                        .foregroundStyle(.primary)
-                        Text("\(units.rawValue)")
-                            .font(.footnote)
-                            .foregroundStyle(.secondary)
                     }
                 } else {
                     Text("---")
-                        .font(.footnote)
-                        .foregroundStyle(.primary)
-                    Text("\(units.rawValue)")
-                        .font(.footnote)
-                        .foregroundStyle(.secondary)
                 }
             }
+            .font(.footnote)
+            .foregroundStyle(.primary)
+            .padding(8)
+            .background {
+                RoundedRectangle(cornerRadius: 10)
+                    .fill(Color.primary.opacity(0.2))
+            }
         }
     }
 
@@ -122,9 +116,20 @@ struct ForeCastChart: View {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-            let pointMarkColor: Color = glucoseToDisplay > state.highGlucose ? Color.orange :
-                glucoseToDisplay < state.lowGlucose ? Color.red :
-                Color.green
+
+            // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+            let lowGlucose = units == .mgdL ? state.lowGlucose : state.lowGlucose.asMgdL
+            let highGlucose = units == .mgdL ? state.highGlucose : state.highGlucose.asMgdL
+            let targetGlucose = (state.determination.first?.currentTarget ?? state.currentBGTarget as NSDecimalNumber) as Decimal
+
+            let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
+                glucoseValue: Decimal(item.glucose),
+                highGlucoseColorValue: highGlucose,
+                lowGlucoseColorValue: lowGlucose,
+                targetGlucose: targetGlucose,
+                glucoseColorScheme: state.glucoseColorScheme,
+                offset: 20
+            )
 
             if !state.isSmoothingEnabled {
                 PointMark(
@@ -132,7 +137,7 @@ struct ForeCastChart: View {
                     y: .value("Value", glucoseToDisplay)
                 )
                 .foregroundStyle(pointMarkColor)
-                .symbolSize(20)
+                .symbolSize(18)
             } else {
                 PointMark(
                     x: .value("Time", item.date ?? Date(), unit: .second),
@@ -140,7 +145,7 @@ struct ForeCastChart: View {
                 )
                 .symbol {
                     Image(systemName: "record.circle.fill")
-                        .font(.system(size: 8))
+                        .font(.system(size: 6))
                         .bold()
                         .foregroundStyle(pointMarkColor)
                 }
@@ -232,8 +237,8 @@ struct ForeCastChart: View {
         AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
-                .font(.footnote)
-                .foregroundStyle(Color.primary)
+                .font(.caption2)
+                .foregroundStyle(Color.secondary)
         }
     }
 
@@ -241,7 +246,7 @@ struct ForeCastChart: View {
         AxisMarks(position: .trailing) { _ in
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
-            AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
+            AxisValueLabel().font(.caption2).foregroundStyle(Color.secondary)
         }
     }
 }

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

@@ -1,5 +1,6 @@
 import CoreData
 import Foundation
+import HealthKit
 import SwiftUI
 
 enum DataTable {
@@ -218,6 +219,10 @@ enum DataTable {
 }
 
 protocol DataTableProvider: Provider {
-    func deleteCarbsFromNightscout(withID id: String) async
-    func deleteInsulin(with treatmentObjectID: NSManagedObjectID) async
+    func deleteCarbsFromNightscout(withID id: String)
+    func deleteInsulinFromNightscout(withID id: String)
+    func deleteManualGlucoseFromNightscout(withID id: String)
+    func deleteGlucoseFromHealth(withSyncID id: String)
+    func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType)
+    func deleteInsulinFromHealth(withSyncID id: String)
 }

+ 36 - 34
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -1,5 +1,6 @@
 import CoreData
 import Foundation
+import HealthKit
 
 extension DataTable {
     final class Provider: BaseProvider, DataTableProvider {
@@ -14,52 +15,53 @@ extension DataTable {
         }
 
         func deleteCarbsFromNightscout(withID id: String) {
-            Task {
-                await nightscoutManager.deleteCarbs(withID: id)
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.nightscoutManager.deleteCarbs(withID: id)
             }
         }
 
-        func deleteInsulin(with treatmentObjectID: NSManagedObjectID) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-
-            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
-                    }
-
-                    // 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)
+        func deleteInsulinFromNightscout(withID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.nightscoutManager.deleteInsulin(withID: id)
+            }
+        }
 
-                    taskContext.delete(treatmentToDelete)
-                    try taskContext.save()
+        func deleteInsulinFromHealth(withSyncID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.healthkitManager.deleteInsulin(syncID: id)
+            }
+        }
 
-                    debug(.default, "Successfully deleted the treatment object.")
-                } catch {
-                    debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
-                }
+        func deleteManualGlucoseFromNightscout(withID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.nightscoutManager.deleteManualGlucose(withID: id)
             }
         }
 
-        func deleteInsulinFromNightscout(withID id: String) {
-            Task {
-                await nightscoutManager.deleteInsulin(withID: id)
+        func deleteGlucoseFromHealth(withSyncID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.healthkitManager.deleteGlucose(syncID: id)
             }
         }
 
-        func deleteManualGlucose(withID id: String) {
-            Task {
-                await nightscoutManager.deleteManualGlucose(withID: id)
+        func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.healthkitManager.deleteMealData(byID: id, sampleType: sampleType)
             }
         }
+
+        func deleteInsulinFromTidepool(withSyncId id: String, amount: Decimal, at: Date) {
+            tidepoolManager.deleteInsulin(withSyncId: id, amount: amount, at: at)
+        }
+
+        func deleteCarbsFromTidepool(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String) {
+            tidepoolManager.deleteCarbs(withSyncId: id, carbs: carbs, at: at, enteredBy: enteredBy)
+        }
     }
 }

+ 120 - 45
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -1,4 +1,5 @@
 import CoreData
+import HealthKit
 import SwiftUI
 
 extension DataTable {
@@ -37,19 +38,26 @@ extension DataTable {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
 
-        // Carb and FPU deletion from history
-        /// 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 invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        // Glucose deletion from history and from remote services
+        /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
                 await deleteGlucose(treatmentObjectID)
             }
         }
 
         func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+            // Delete from Apple Health/Tidepool
+            await deleteGlucoseFromServices(treatmentObjectID)
+
+            // Delete from Core Data
+            await glucoseStorage.deleteGlucose(treatmentObjectID)
+        }
+
+        func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteGlucose"
+            taskContext.transactionAuthor = "deleteGlucoseFromServices"
 
             await taskContext.perform {
                 do {
@@ -60,41 +68,55 @@ extension DataTable {
                         return
                     }
 
-                    // Delete Manual Glucose from Nightscout
-                    if glucoseToDelete.isManual == true {
-                        if let id = glucoseToDelete.id?.uuidString {
-                            self.provider.deleteManualGlucose(withID: id)
-                        }
+                    // Delete from Nightscout
+                    if let id = glucoseToDelete.id?.uuidString {
+                        self.provider.deleteManualGlucoseFromNightscout(withID: id)
                     }
 
-                    taskContext.delete(glucoseToDelete)
+                    // Delete from Apple Health
+                    if let id = glucoseToDelete.id?.uuidString {
+                        self.provider.deleteGlucoseFromHealth(withSyncID: id)
+                    }
 
-                    guard taskContext.hasChanges else { return }
-                    try taskContext.save()
-                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
+                    debugPrint(
+                        "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
+                    )
                 } catch {
                     debugPrint(
-                        "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
+                        "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
                     )
                 }
             }
         }
 
         // Carb and FPU deletion from history
-        /// 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 invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
                 await deleteCarbs(treatmentObjectID)
-                carbEntryDeleted = true
-                waitForSuggestion = true
+
+                await MainActor.run {
+                    carbEntryDeleted = true
+                    waitForSuggestion = true
+                }
             }
         }
 
         func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+            // Delete from Apple Health/Tidepool
+            await deleteCarbsFromServices(treatmentObjectID)
+
+            // Delete carbs from Core Data
+            await carbsStorage.deleteCarbs(treatmentObjectID)
+
+            // Perform a determine basal sync to update cob
+            await apsManager.determineBasalSync()
+        }
+
+        func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteCarbs"
+            taskContext.transactionAuthor = "deleteCarbsFromServices"
 
             var carbEntry: CarbEntryStored?
 
@@ -103,46 +125,66 @@ extension DataTable {
                 do {
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     guard let carbEntry = carbEntry else {
-                        debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
+                        debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
                         return
                     }
 
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
-                        // Delete FPUs from Nightscout
+                        // Delete Fat and Protein entries from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
+
+                        // Delete Fat and Protein entries from Apple Health
+                        let healthObjectsToDelete: [HKSampleType?] = [
+                            AppleHealthConfig.healthFatObject,
+                            AppleHealthConfig.healthProteinObject
+                        ]
+
+                        for sampleType in healthObjectsToDelete {
+                            if let validSampleType = sampleType {
+                                self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
+                            }
+                        }
                     } else {
                         // Delete carbs from Nightscout
-                        if let id = carbEntry.id?.uuidString {
-                            self.provider.deleteCarbsFromNightscout(withID: id)
+                        if let id = carbEntry.id, let entryDate = carbEntry.date {
+                            self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
+
+                            // Delete carbs from Apple Health
+                            if let sampleType = AppleHealthConfig.healthCarbObject {
+                                self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
+                            }
+
+                            self.provider.deleteCarbsFromTidepool(
+                                withSyncId: id,
+                                carbs: Decimal(carbEntry.carbs),
+                                at: entryDate,
+                                enteredBy: CarbsEntry.manual
+                            )
                         }
                     }
 
                 } catch {
                     debugPrint(
-                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from Nightscout: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
                     )
                 }
             }
-
-            // Delete carbs from Core Data
-            await carbsStorage.deleteCarbs(treatmentObjectID)
-
-            // Perform a determine basal sync to update cob
-            await apsManager.determineBasalSync()
         }
 
         // Insulin deletion from history
-        /// 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 invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
-                await deleteInsulin(treatmentObjectID)
-                insulinEntryDeleted = true
-                waitForSuggestion = true
+                await invokeInsulinDeletion(treatmentObjectID)
+
+                await MainActor.run {
+                    insulinEntryDeleted = true
+                    waitForSuggestion = true
+                }
             }
         }
 
-        func deleteInsulin(_ treatmentObjectID: NSManagedObjectID) async {
+        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
             do {
                 let authenticated = try await unlockmanager.unlock()
 
@@ -151,14 +193,14 @@ extension DataTable {
                     return
                 }
 
-                async let deleteNSManagedObjectTask: () = CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
-                async let deleteInsulinFromNightScoutTask: () = provider.deleteInsulin(with: treatmentObjectID)
-                async let determineBasalTask: () = apsManager.determineBasalSync()
+                // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
+                await deleteInsulinFromServices(with: treatmentObjectID)
 
-                await deleteNSManagedObjectTask
-                await deleteInsulinFromNightScoutTask
-                await determineBasalTask
+                // Delete from Core Data
+                await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
+                // Perform a determine basal sync to update iob
+                await apsManager.determineBasalSync()
             } catch {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
@@ -166,6 +208,37 @@ extension DataTable {
             }
         }
 
+        func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
+            let taskContext = CoreDataStack.shared.newTaskContext()
+            taskContext.name = "deleteContext"
+            taskContext.transactionAuthor = "deleteInsulinFromServices"
+
+            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
+                    }
+
+                    if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
+                       let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
+                    {
+                        self.provider.deleteInsulinFromNightscout(withID: id)
+                        self.provider.deleteInsulinFromHealth(withSyncID: id)
+                        self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
+                    }
+
+                    taskContext.delete(treatmentToDelete)
+                    try taskContext.save()
+
+                    debug(.default, "Successfully deleted the treatment object.")
+                } catch {
+                    debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
+                }
+            }
+        }
+
         func addManualGlucose() {
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let glucoseAsInt = Int(glucose)
@@ -178,6 +251,8 @@ extension DataTable {
                 newItem.glucose = Int16(glucoseAsInt)
                 newItem.isManual = true
                 newItem.isUploadedToNS = false
+                newItem.isUploadedToHealth = false
+                newItem.isUploadedToTidepool = false
 
                 do {
                     guard self.coredataContext.hasChanges else { return }

Plik diff jest za duży
+ 18 - 18
FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift


+ 16 - 14
FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift

@@ -14,7 +14,7 @@ extension AppleHealthKit {
 
             useAppleHealth = settingsManager.settings.useAppleHealth
 
-            needShowInformationTextForSetPermissions = healthKitManager.areAllowAllPermissions
+            needShowInformationTextForSetPermissions = healthKitManager.hasGrantedFullWritePermissions
 
             subscribeSetting(\.useAppleHealth, on: $useAppleHealth) {
                 useAppleHealth = $0
@@ -26,20 +26,22 @@ extension AppleHealthKit {
                     return
                 }
 
-                self.healthKitManager.requestPermission { ok, error in
-                    DispatchQueue.main.async {
-                        self.needShowInformationTextForSetPermissions = !self.healthKitManager.checkAvailabilitySaveBG()
+                Task {
+                    do {
+                        let permissionGranted = try await self.healthKitManager.requestPermission()
+
+                        await MainActor.run {
+                            self.needShowInformationTextForSetPermissions = !self.healthKitManager.hasGlucoseWritePermission()
+                        }
+
+                        if permissionGranted {
+                            debug(.service, "Permission granted for HealthKitManager")
+                        } else {
+                            warning(.service, "Permission not granted for HealthKitManager")
+                        }
+                    } catch {
+                        warning(.service, "Error requesting permission for HealthKitManager", error: error)
                     }
-
-                    guard ok, error == nil else {
-                        warning(.service, "Permission not granted for HealthKitManager", error: error)
-                        return
-                    }
-
-                    debug(.service, "Permission  granted HealthKitManager")
-
-                    self.healthKitManager.createBGObserver()
-                    self.healthKitManager.enableBackgroundDelivery()
                 }
             }
         }

+ 32 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift

@@ -0,0 +1,32 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupBatteryArray() {
+        Task {
+            let ids = await self.fetchBattery()
+            let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateBatteryArray(with: batteryObjects)
+        }
+    }
+
+    private func fetchBattery() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OpenAPS_Battery.self,
+            onContext: batteryFetchContext,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false
+        )
+
+        return await batteryFetchContext.perform {
+            guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
+
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
+        batteryFromPersistence = objects
+    }
+}

+ 60 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift

@@ -0,0 +1,60 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupCarbsArray() {
+        Task {
+            let ids = await self.fetchCarbs()
+            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateCarbsArray(with: carbObjects)
+        }
+    }
+
+    private func fetchCarbs() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: carbsFetchContext,
+            predicate: NSPredicate.carbsForChart,
+            key: "date",
+            ascending: false
+        )
+
+        return await carbsFetchContext.perform {
+            guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
+        carbsFromPersistence = objects
+    }
+
+    func setupFPUsArray() {
+        Task {
+            let ids = await self.fetchFPUs()
+            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateFPUsArray(with: fpuObjects)
+        }
+    }
+
+    private func fetchFPUs() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: fpuFetchContext,
+            predicate: NSPredicate.fpusForChart,
+            key: "date",
+            ascending: false
+        )
+
+        return await fpuFetchContext.perform {
+            guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
+        fpusFromPersistence = objects
+    }
+}

+ 103 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift

@@ -0,0 +1,103 @@
+import Foundation
+
+extension Home.StateModel {
+    func yAxisChartData(glucoseValues: [GlucoseStored]) {
+        // Capture the forecast values from `preprocessedData` on the main thread
+        Task { @MainActor in
+            let forecastValues = self.preprocessedData.map { Decimal($0.forecastValue.value) }
+
+            // Perform the glucose processing on the background context
+            glucoseFetchContext.perform {
+                let glucoseMapped = glucoseValues.map { Decimal($0.glucose) }
+
+                // Calculate min and max values for glucose and forecast
+                let minGlucose = glucoseMapped.min()
+                let maxGlucose = glucoseMapped.max()
+                let minForecast = forecastValues.min()
+                let maxForecast = forecastValues.max()
+
+                // Ensure all values exist, otherwise set default values
+                guard let minGlucose = minGlucose, let maxGlucose = maxGlucose,
+                      let minForecast = minForecast, let maxForecast = maxForecast
+                else {
+                    Task {
+                        await self.updateChartBounds(minValue: 39, maxValue: 300)
+                    }
+                    return
+                }
+
+                // Adjust max forecast to be no more than 100 over max glucose
+                let adjustedMaxForecast = min(maxForecast, maxGlucose + 100)
+                let minOverall = min(minGlucose, minForecast)
+                let maxOverall = max(maxGlucose, adjustedMaxForecast)
+
+                // Update the chart bounds on the main thread
+                Task {
+                    await self.updateChartBounds(minValue: minOverall - 50, maxValue: maxOverall + 80)
+                }
+            }
+        }
+    }
+
+    @MainActor private func updateChartBounds(minValue: Decimal, maxValue: Decimal) async {
+        minYAxisValue = minValue
+        maxYAxisValue = maxValue
+    }
+
+    func yAxisChartDataCobChart(determinations: [[String: Any]]) {
+        determinationFetchContext.perform {
+            // Map the COB values from the dictionary results
+            let cobMapped = determinations.compactMap { entry in
+                // First cast to Int16, then convert to Decimal
+                if let cobValue = entry["cob"] as? Int16 {
+                    return Decimal(cobValue)
+                }
+                return nil
+            }
+            let maxCob = cobMapped.max()
+
+            // Ensure the result exists or set default values
+            if let maxCob = maxCob {
+                let calculatedMax = maxCob == 0 ? 20 : maxCob + 20
+                Task {
+                    await self.updateCobChartBounds(minValue: 0, maxValue: calculatedMax)
+                }
+            } else {
+                Task {
+                    await self.updateCobChartBounds(minValue: 0, maxValue: 20)
+                }
+            }
+        }
+    }
+
+    @MainActor private func updateCobChartBounds(minValue: Decimal, maxValue: Decimal) {
+        minValueCobChart = minValue
+        maxValueCobChart = maxValue
+    }
+
+    func yAxisChartDataIobChart(determinations: [[String: Any]]) {
+        determinationFetchContext.perform {
+            // Map the IOB values from the fetched dictionaries
+            let iobMapped = determinations.compactMap { ($0["iob"] as? NSDecimalNumber)?.decimalValue }
+            let minIob = iobMapped.min()
+            let maxIob = iobMapped.max()
+
+            // Ensure min and max IOB values exist, or set defaults
+            if let minIob = minIob, let maxIob = maxIob {
+                let adjustedMin = minIob < 0 ? minIob - 2 : 0
+                Task {
+                    await self.updateIobChartBounds(minValue: adjustedMin, maxValue: maxIob + 2)
+                }
+            } else {
+                Task {
+                    await self.updateIobChartBounds(minValue: 0, maxValue: 5)
+                }
+            }
+        }
+    }
+
+    @MainActor private func updateIobChartBounds(minValue: Decimal, maxValue: Decimal) async {
+        minValueIobChart = minValue
+        maxValueIobChart = maxValue
+    }
+}

+ 58 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift

@@ -0,0 +1,58 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupDeterminationsArray() {
+        Task {
+            // Get the NSManagedObjectIDs
+            async let enactedObjectIds = determinationStorage
+                .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
+            async let enactedAndNonEnactedObjectIds = fetchCobAndIob()
+
+            let enactedIDs = await enactedObjectIds
+            let enactedAndNonEnactedIds = await enactedAndNonEnactedObjectIds
+
+            // Get the NSManagedObjects and return them on the Main Thread
+            await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
+            await updateDeterminationsArray(with: enactedAndNonEnactedIds, keyPath: \.enactedAndNonEnactedDeterminations)
+
+            await updateForecastData()
+        }
+    }
+
+    @MainActor private func updateDeterminationsArray(
+        with IDs: [NSManagedObjectID],
+        keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
+    ) async {
+        // Fetch the objects off the main thread
+        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
+            .getNSManagedObject(with: IDs, context: viewContext)
+
+        // Update the array on the main thread
+        self[keyPath: keyPath] = determinationObjects
+    }
+
+    // Custom fetch to more efficiently filter only for cob and iob
+    private func fetchCobAndIob() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: determinationFetchContext,
+            predicate: NSPredicate.determinationsForCobIobCharts,
+            key: "deliverAt",
+            ascending: false,
+            batchSize: 50,
+            propertiesToFetch: ["cob", "iob", "objectID"]
+        )
+
+        return await determinationFetchContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else {
+                return []
+            }
+
+            // Update Chart Scales
+            self.yAxisChartDataCobChart(determinations: fetchedResults)
+            self.yAxisChartDataIobChart(determinations: fetchedResults)
+            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+        }
+    }
+}

+ 106 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -0,0 +1,106 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    // Asynchronously preprocess Forecast data in a background thread
+    func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
+        // Get the Determination ID on the main context
+        guard let id = await viewContext.perform({ self.enactedAndNonEnactedDeterminations.first?.objectID }) else {
+            return []
+        }
+
+        // Get the Forecast IDs for the Determination ID
+        // Here we can safely use a background context since we are using the NSManagedObjectID
+        let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: taskContext)
+
+        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+        // Use a task group to fetch Forecast VALUE IDs concurrently
+        await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
+            for forecastID in forecastIDs {
+                group.addTask {
+                    // Fetch forecast value IDs asynchronously (but outside of perform)
+                    let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
+                        for: forecastID,
+                        in: self.taskContext
+                    )
+                    return (UUID(), forecastID, forecastValueIDs)
+                }
+            }
+
+            // Collect the results from the task group
+            for await (uuid, forecastID, forecastValueIDs) in group {
+                result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
+            }
+        }
+
+        return result
+    }
+
+    // Update forecast data and UI on the main thread
+    @MainActor func updateForecastData() async {
+        // Preprocess forecast data on a background thread
+        let forecastDataIDs = await preprocessForecastData()
+
+        // Use an Array of Int instead of ForecastValues to be able to pass values thread safe
+        var allForecastValues = [[Int]]()
+        var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
+
+        // Use a task group to fetch forecast values concurrently
+        await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
+            for data in forecastDataIDs {
+                group.addTask {
+                    await self.determinationStorage
+                        .fetchForecastObjects(
+                            for: data,
+                            in: self.viewContext
+                        ) // This directly returns NSManagedobjects on the Main Thread
+                }
+            }
+
+            // Collect the results from the task group
+            for await (id, forecast, forecastValues) in group {
+                guard let forecast = forecast, !forecastValues.isEmpty else { continue }
+
+                // Extract only the 'value' from ForecastValue on the main thread
+                let forecastValueInts = forecastValues
+                    .compactMap { Int($0.value) }
+                allForecastValues.append(forecastValueInts)
+                preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
+            }
+        }
+
+        // Update Array on the Main Thread
+        self.preprocessedData = preprocessedData
+
+        // Ensure there are forecast values to process
+        guard !allForecastValues.isEmpty else {
+            minForecast = []
+            maxForecast = []
+            return
+        }
+
+        minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
+        guard minCount > 0 else { return }
+
+        // Copy allForecastValues to a local constant for thread safety
+        let localAllForecastValues = allForecastValues
+
+        // Calculate min and max forecast values in a background task
+        let (minResult, maxResult) = await Task.detached {
+            let minForecast = (0 ..< self.minCount).map { index in
+                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
+            }
+
+            let maxForecast = (0 ..< self.minCount).map { index in
+                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
+            }
+
+            return (minForecast, maxForecast)
+        }.value
+
+        // Update the properties on the main thread
+        minForecast = minResult
+        maxForecast = maxResult
+    }
+}

+ 38 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -0,0 +1,38 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupGlucoseArray() {
+        Task {
+            let ids = await self.fetchGlucose()
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateGlucoseArray(with: glucoseObjects)
+        }
+    }
+
+    private func fetchGlucose() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: glucoseFetchContext,
+            predicate: NSPredicate.glucose,
+            key: "date",
+            ascending: true,
+            fetchLimit: 288
+        )
+
+        return await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+            // Update Main Chart Y Axis Values
+            // Perform everything on "context" to be thread safe
+            self.yAxisChartData(glucoseValues: fetchedResults)
+
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+        glucoseFromPersistence = objects
+        latestTwoGlucoseValues = Array(objects.suffix(2))
+    }
+}

+ 114 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift

@@ -0,0 +1,114 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    // Setup Overrides
+    func setupOverrides() {
+        Task {
+            let ids = await self.fetchOverrides()
+            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateOverrideArray(with: overrideObjects)
+        }
+    }
+
+    private func fetchOverrides() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: overrideFetchContext,
+            predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
+            key: "date",
+            ascending: false
+        )
+
+        return await overrideFetchContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
+        overrides = objects
+    }
+
+    @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
+    func setupOverrideRunStored() {
+        Task {
+            let ids = await self.fetchOverrideRunStored()
+            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateOverrideRunStoredArray(with: overrideRunObjects)
+        }
+    }
+
+    private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideRunStored.self,
+            onContext: overrideFetchContext,
+            predicate: predicate,
+            key: "startDate",
+            ascending: false
+        )
+
+        return await overrideFetchContext.perform {
+            guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
+
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
+        overrideRunStored = objects
+    }
+
+    @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.name = object.name
+                newOverrideRunStored.startDate = object.date ?? .distantPast
+                newOverrideRunStored.endDate = Date()
+                newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
+                newOverrideRunStored.override = object
+                newOverrideRunStored.isUploadedToNS = false
+
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
+            }
+        }
+    }
+
+    @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
+        do {
+            let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
+            profileToCancel?.enabled = false
+
+            await saveToOverrideRunStored(withID: id)
+
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+
+            Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
+        }
+    }
+}

+ 81 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -0,0 +1,81 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupInsulinArray() {
+        Task {
+            let ids = await self.fetchInsulin()
+            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
+            await updateInsulinArray(with: insulinObjects)
+        }
+    }
+
+    private func fetchInsulin() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: NSPredicate.pumpHistoryLast24h,
+            key: "timestamp",
+            ascending: true
+        )
+
+        return await pumpHistoryFetchContext.perform {
+            guard let pumpEvents = results as? [PumpEventStored] else {
+                return []
+            }
+
+            return pumpEvents.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
+        insulinFromPersistence = insulinObjects
+
+        manualTempBasal = apsManager.isManualTempBasal
+        tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
+
+        suspensions = insulinFromPersistence.filter {
+            $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
+        }
+        let lastSuspension = suspensions.last
+
+        pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
+            .type == EventType.pumpSuspend.rawValue
+    }
+
+    // Setup Last Bolus to display the bolus progress bar
+    // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
+    func setupLastBolus() {
+        Task {
+            guard let id = await self.fetchLastBolus() else { return }
+            await updateLastBolus(with: id)
+        }
+    }
+
+    func fetchLastBolus() async -> NSManagedObjectID? {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: NSPredicate.lastPumpBolus,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        return await pumpHistoryFetchContext.perform {
+            guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
+
+            return fetchedResults.map(\.objectID).first
+        }
+    }
+
+    @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
+        do {
+            lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
+        } catch {
+            debugPrint(
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
+            )
+        }
+    }
+}

+ 103 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -0,0 +1,103 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupTempTargetsStored() {
+        Task {
+            let ids = await self.fetchTempTargets()
+            let tempTargetObjects: [TempTargetStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateTempTargetsArray(with: tempTargetObjects)
+        }
+    }
+
+    private func fetchTempTargets() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: tempTargetFetchContext,
+            predicate: NSPredicate.lastActiveTempTarget,
+            key: "date",
+            ascending: false
+        )
+
+        return await tempTargetFetchContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateTempTargetsArray(with objects: [TempTargetStored]) {
+        tempTargetStored = objects
+    }
+
+    // Setup expired TempTargets
+    func setupTempTargetsRunStored() {
+        Task {
+            let ids = await self.fetchTempTargetRunStored()
+            let tempTargetRunObjects: [TempTargetRunStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+        }
+    }
+
+    private func fetchTempTargetRunStored() async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetRunStored.self,
+            onContext: tempTargetFetchContext,
+            predicate: predicate,
+            key: "startDate",
+            ascending: false
+        )
+
+        return await tempTargetFetchContext.perform {
+            guard let fetchedResults = results as? [TempTargetRunStored] else { return [] }
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateTempTargetRunStoredArray(with objects: [TempTargetRunStored]) {
+        tempTargetRunStored = objects
+    }
+
+    @MainActor func saveToTempTargetRunStored(withID id: NSManagedObjectID) async {
+        await viewContext.perform {
+            do {
+                guard let object = try self.viewContext.existingObject(with: id) as? TempTargetStored else { return }
+
+                let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
+                newTempTargetRunStored.id = UUID()
+                newTempTargetRunStored.name = object.name
+                newTempTargetRunStored.startDate = object.date ?? .distantPast
+                newTempTargetRunStored.endDate = Date()
+                newTempTargetRunStored.target = object.target ?? 0
+                newTempTargetRunStored.tempTarget = object
+                newTempTargetRunStored.isUploadedToNS = false
+
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object"
+                )
+            }
+        }
+    }
+
+    @MainActor func cancelTempTarget(withID id: NSManagedObjectID) async {
+        do {
+            let profileToCancel = try viewContext.existingObject(with: id) as? TempTargetStored
+            profileToCancel?.enabled = false
+
+            await saveToTempTargetRunStored(withID: id)
+
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+
+            // We also need to update the storage for temp targets
+            tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
+
+            Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
+        }
+    }
+}

+ 76 - 596
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -47,7 +47,9 @@ extension Home {
         @Published var maxValue: Decimal = 1.2
         @Published var lowGlucose: Decimal = 70
         @Published var highGlucose: Decimal = 180
+        @Published var currentGlucoseTarget: Decimal = 100
         @Published var overrideUnit: Bool = false
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var displayXgridLines: Bool = false
         @Published var displayYgridLines: Bool = false
         @Published var thresholdLines: Bool = false
@@ -89,7 +91,24 @@ extension Home {
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var forecastDisplayType: ForecastDisplayType = .cone
 
-        let context = CoreDataStack.shared.newTaskContext()
+        @Published var minYAxisValue: Decimal = 39
+        @Published var maxYAxisValue: Decimal = 300
+
+        @Published var minValueCobChart: Decimal = 0
+        @Published var maxValueCobChart: Decimal = 20
+
+        @Published var minValueIobChart: Decimal = 0
+        @Published var maxValueIobChart: Decimal = 5
+
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
+        let carbsFetchContext = CoreDataStack.shared.newTaskContext()
+        let fpuFetchContext = CoreDataStack.shared.newTaskContext()
+        let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+        let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
+        let overrideFetchContext = CoreDataStack.shared.newTaskContext()
+        let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
+        let batteryFetchContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
@@ -104,6 +123,9 @@ extension Home {
                     .share()
                     .eraseToAnyPublisher()
 
+            registerSubscribers()
+            registerHandlers()
+
             // Parallelize Setup functions
             setupHomeViewConcurrently()
         }
@@ -112,12 +134,6 @@ extension Home {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
-                        self.registerSubscribers()
-                    }
-                    group.addTask {
-                        self.registerHandlers()
-                    }
-                    group.addTask {
                         self.setupGlucoseArray()
                     }
                     group.addTask {
@@ -371,6 +387,7 @@ extension Home {
             alarm = provider.glucoseStorage.alarm
             manualTempBasal = apsManager.isManualTempBasal
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            glucoseColorScheme = settingsManager.settings.glucoseColorScheme
             maxValue = settingsManager.preferences.autosensMax
             lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
             highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
@@ -423,41 +440,6 @@ extension Home {
             }
         }
 
-        @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
-            do {
-                let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
-                profileToCancel?.enabled = false
-
-                await saveToOverrideRunStored(withID: id)
-
-                guard viewContext.hasChanges else { return }
-                try viewContext.save()
-
-                Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
-            }
-        }
-
-        @MainActor func cancelTempTarget(withID id: NSManagedObjectID) async {
-            do {
-                let profileToCancel = try viewContext.existingObject(with: id) as? TempTargetStored
-                profileToCancel?.enabled = false
-
-                await saveToTempTargetRunStored(withID: id)
-
-                guard viewContext.hasChanges else { return }
-                try viewContext.save()
-
-                // We also need to update the storage for temp targets
-                tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
-
-                Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
-            }
-        }
-
         func calculateTINS() -> String {
             let startTime = calculateStartTime(hours: Int(hours))
 
@@ -536,6 +518,54 @@ extension Home {
             }
         }
 
+        private func getCurrentGlucoseTarget() async {
+            let now = Date()
+            let calendar = Calendar.current
+            let dateFormatter = DateFormatter()
+            dateFormatter.dateFormat = "HH:mm:ss"
+            dateFormatter.timeZone = TimeZone.current
+
+            let bgTargets = await provider.getBGTarget()
+            let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
+
+            for (index, entry) in entries.enumerated() {
+                guard let entryTime = dateFormatter.date(from: entry.start) else {
+                    print("Invalid entry start time: \(entry.start)")
+                    continue
+                }
+
+                let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+                let entryStartTime = calendar.date(
+                    bySettingHour: entryComponents.hour!,
+                    minute: entryComponents.minute!,
+                    second: entryComponents.second!,
+                    of: now
+                )!
+
+                let entryEndTime: Date
+                if index < entries.count - 1,
+                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
+                {
+                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                    entryEndTime = calendar.date(
+                        bySettingHour: nextEntryComponents.hour!,
+                        minute: nextEntryComponents.minute!,
+                        second: nextEntryComponents.second!,
+                        of: now
+                    )!
+                } else {
+                    entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+                }
+
+                if now >= entryStartTime, now < entryEndTime {
+                    await MainActor.run {
+                        currentGlucoseTarget = units == .mgdL ? entry.value : entry.value.asMmolL
+                    }
+                    return
+                }
+            }
+        }
+
         func openCGM() {
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
         }
@@ -569,7 +599,11 @@ extension Home.StateModel:
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
         lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
         highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+        Task {
+            await getCurrentGlucoseTarget()
+        }
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
+        glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
@@ -636,557 +670,3 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // TODO:
     }
 }
-
-// MARK: - Handle Core Data changes and update Arrays to display them in the UI
-
-extension Home.StateModel {
-    // Setup Glucose
-    private func setupGlucoseArray() {
-        Task {
-            let ids = await self.fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateGlucoseArray(with: glucoseObjects)
-        }
-    }
-
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.glucose,
-            key: "date",
-            ascending: true,
-            fetchLimit: 288
-        )
-
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
-        glucoseFromPersistence = objects
-        latestTwoGlucoseValues = Array(objects.suffix(2))
-    }
-
-    // Setup Carbs
-    private func setupCarbsArray() {
-        Task {
-            let ids = await self.fetchCarbs()
-            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateCarbsArray(with: carbObjects)
-        }
-    }
-
-    private func fetchCarbs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: context,
-            predicate: NSPredicate.carbsForChart,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
-        carbsFromPersistence = objects
-    }
-
-    // Setup FPUs
-    private func setupFPUsArray() {
-        Task {
-            let ids = await self.fetchFPUs()
-            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateFPUsArray(with: fpuObjects)
-        }
-    }
-
-    private func fetchFPUs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: context,
-            predicate: NSPredicate.fpusForChart,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
-        fpusFromPersistence = objects
-    }
-
-    // Custom fetch to more efficiently filter only for cob and iob
-    private func fetchCobAndIob() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: context,
-            predicate: NSPredicate.determinationsForCobIobCharts,
-            key: "deliverAt",
-            ascending: false,
-            batchSize: 50,
-            propertiesToFetch: ["cob", "iob", "objectID"]
-        )
-
-        guard let fetchedResults = results as? [[String: Any]] else {
-            return []
-        }
-
-        return await context.perform {
-            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
-        }
-    }
-
-    // Setup Determinations
-    private func setupDeterminationsArray() {
-        Task {
-            // Get the NSManagedObjectIDs
-            async let enactedObjectIDs = determinationStorage
-                .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
-            async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
-
-            let enactedIDs = await enactedObjectIDs
-            let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
-
-            // Get the NSManagedObjects and return them on the Main Thread
-            await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
-            await updateDeterminationsArray(with: enactedAndNonEnactedIDs, keyPath: \.enactedAndNonEnactedDeterminations)
-
-            await updateForecastData()
-        }
-    }
-
-    @MainActor private func updateDeterminationsArray(
-        with IDs: [NSManagedObjectID],
-        keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) async {
-        // Fetch the objects off the main thread
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: IDs, context: viewContext)
-
-        // Update the array on the main thread
-        self[keyPath: keyPath] = determinationObjects
-    }
-
-    // Setup Insulin
-    private func setupInsulinArray() {
-        Task {
-            let ids = await self.fetchInsulin()
-            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateInsulinArray(with: insulinObjects)
-        }
-    }
-
-    private func fetchInsulin() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: context,
-            predicate: NSPredicate.pumpHistoryLast24h,
-            key: "timestamp",
-            ascending: true
-        )
-
-        guard let pumpEvents = results as? [PumpEventStored] else {
-            return []
-        }
-
-        return await context.perform {
-            return pumpEvents.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
-        insulinFromPersistence = insulinObjects
-
-        // Filter tempbasals
-        manualTempBasal = apsManager.isManualTempBasal
-        tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
-
-        // Suspension and resume events
-        suspensions = insulinFromPersistence.filter {
-            $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
-        }
-        let lastSuspension = suspensions.last
-
-        pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
-            .type == EventType.pumpSuspend.rawValue
-    }
-
-    // Setup Last Bolus to display the bolus progress bar
-    // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
-    private func setupLastBolus() {
-        Task {
-            guard let id = await self.fetchLastBolus() else { return }
-            await updateLastBolus(with: id)
-        }
-    }
-
-    private func fetchLastBolus() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: context,
-            predicate: NSPredicate.lastPumpBolus,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1
-        )
-
-        guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID).first
-        }
-    }
-
-    @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
-        do {
-            lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
-            )
-        }
-    }
-
-    // Setup Battery
-    private func setupBatteryArray() {
-        Task {
-            let ids = await self.fetchBattery()
-            let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateBatteryArray(with: batteryObjects)
-        }
-    }
-
-    private func fetchBattery() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OpenAPS_Battery.self,
-            onContext: context,
-            predicate: NSPredicate.predicateFor30MinAgo,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
-        batteryFromPersistence = objects
-    }
-}
-
-extension Home.StateModel {
-    // Setup Overrides
-    private func setupOverrides() {
-        Task {
-            let ids = await self.fetchOverrides()
-            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideArray(with: overrideObjects)
-        }
-    }
-
-    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
-        )
-
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
-        overrides = objects
-    }
-
-    @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()
-            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideRunStoredArray(with: overrideRunObjects)
-        }
-    }
-
-    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
-        )
-
-        guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
-        overrideRunStored = objects
-    }
-
-    // Setup active TempTargets
-    private func setupTempTargetsStored() {
-        Task {
-            let ids = await self.fetchTempTargets()
-            let tempTargetObjects: [TempTargetStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateTempTargetsArray(with: tempTargetObjects)
-        }
-    }
-
-    private func fetchTempTargets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: TempTargetStored.self,
-            onContext: context,
-            predicate: NSPredicate.lastActiveTempTarget,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateTempTargetsArray(with objects: [TempTargetStored]) {
-        tempTargetStored = objects
-    }
-
-    // Setup expired TempTargets
-    private func setupTempTargetsRunStored() {
-        Task {
-            let ids = await self.fetchTempTargetRunStored()
-            let tempTargetRunObjects: [TempTargetRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
-        }
-    }
-
-    private func fetchTempTargetRunStored() async -> [NSManagedObjectID] {
-        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: TempTargetRunStored.self,
-            onContext: context,
-            predicate: predicate,
-            key: "startDate",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [TempTargetRunStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateTempTargetRunStoredArray(with objects: [TempTargetRunStored]) {
-        tempTargetRunStored = objects
-    }
-
-    @MainActor func saveToTempTargetRunStored(withID id: NSManagedObjectID) async {
-        await viewContext.perform {
-            do {
-                guard let object = try self.viewContext.existingObject(with: id) as? TempTargetStored else { return }
-
-                let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
-                newTempTargetRunStored.id = UUID()
-                newTempTargetRunStored.name = object.name
-                newTempTargetRunStored.startDate = object.date ?? .distantPast
-                newTempTargetRunStored.endDate = Date()
-                newTempTargetRunStored.target = object.target ?? 0
-                newTempTargetRunStored.tempTarget = object
-                newTempTargetRunStored.isUploadedToNS = false
-
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
-            }
-        }
-    }
-
-    @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.name = object.name
-                newOverrideRunStored.startDate = object.date ?? .distantPast
-                newOverrideRunStored.endDate = Date()
-                newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
-                newOverrideRunStored.override = object
-                newOverrideRunStored.isUploadedToNS = false
-
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
-            }
-        }
-    }
-}
-
-extension Home.StateModel {
-    // Asynchronously preprocess forecast data in a background thread
-    func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
-        await Task.detached { [self] () -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] in
-            // Get the first determination ID from persistence
-            guard let id = enactedAndNonEnactedDeterminations.first?.objectID else {
-                return []
-            }
-
-            // Get the forecast IDs for the determination ID
-            let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
-            var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
-
-            // Use a task group to fetch forecast value IDs concurrently
-            await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
-                for forecastID in forecastIDs {
-                    group.addTask {
-                        let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
-                            for: forecastID,
-                            in: self.context
-                        )
-                        return (UUID(), forecastID, forecastValueIDs)
-                    }
-                }
-
-                // Collect the results from the task group
-                for await (uuid, forecastID, forecastValueIDs) in group {
-                    result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
-                }
-            }
-
-            return result
-        }.value
-    }
-
-    // Fetch forecast values for a given data set
-    func fetchForecastValues(
-        for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
-        in context: NSManagedObjectContext
-    ) async -> (UUID, Forecast?, [ForecastValue]) {
-        var forecast: Forecast?
-        var forecastValues: [ForecastValue] = []
-
-        do {
-            try await context.perform {
-                // Fetch the forecast object
-                forecast = try context.existingObject(with: data.forecastID) as? Forecast
-
-                // Fetch the first 3h of forecast values
-                for forecastValueID in data.forecastValueIDs.prefix(36) {
-                    if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
-                        forecastValues.append(forecastValue)
-                    }
-                }
-            }
-        } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
-            )
-        }
-
-        return (data.id, forecast, forecastValues)
-    }
-
-    // Update forecast data and UI on the main thread
-    @MainActor func updateForecastData() async {
-        // Preprocess forecast data on a background thread
-        let forecastData = await preprocessForecastData()
-
-        var allForecastValues = [[ForecastValue]]()
-        var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
-
-        // Use a task group to fetch forecast values concurrently
-        await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
-            for data in forecastData {
-                group.addTask {
-                    await self.fetchForecastValues(for: data, in: self.viewContext)
-                }
-            }
-
-            // Collect the results from the task group
-            for await (id, forecast, forecastValues) in group {
-                guard let forecast = forecast, !forecastValues.isEmpty else { continue }
-
-                allForecastValues.append(forecastValues)
-                preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
-            }
-        }
-
-        self.preprocessedData = preprocessedData
-
-        // Ensure there are forecast values to process
-        guard !allForecastValues.isEmpty else {
-            minForecast = []
-            maxForecast = []
-            return
-        }
-
-        minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
-        guard minCount > 0 else { return }
-
-        // Copy allForecastValues to a local constant for thread safety
-        let localAllForecastValues = allForecastValues
-
-        // Calculate min and max forecast values in a background task
-        let (minResult, maxResult) = await Task.detached {
-            let minForecast = (0 ..< self.minCount).map { index in
-                localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.min() ?? 0
-            }
-
-            let maxForecast = (0 ..< self.minCount).map { index in
-                localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.max() ?? 0
-            }
-
-            return (minForecast, maxForecast)
-        }.value
-
-        // Update the properties on the main thread
-        minForecast = minResult
-        maxForecast = maxResult
-    }
-}

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

@@ -32,7 +32,7 @@ extension MainChartView {
         .backport.chartXSelection(value: $selection)
         .chartXAxis { basalChartXAxis }
         .chartYAxis { cobChartYAxis }
-        .chartYScale(domain: minValueCobChart ... maxValueCobChart)
+        .chartYScale(domain: state.minValueCobChart ... state.maxValueCobChart)
     }
 
     func drawCOB(dummy: Bool) -> some ChartContent {

+ 26 - 4
FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift

@@ -8,9 +8,28 @@ extension MainChartView {
         Chart {
             /// high and low threshold lines
             if thresholdLines {
-                RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
+                let highColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: highGlucose,
+                    highGlucoseColorValue: highGlucose,
+                    lowGlucoseColorValue: highGlucose,
+                    targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
+                    glucoseColorScheme: glucoseColorScheme,
+                    offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
+                )
+                let lowColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: lowGlucose,
+                    highGlucoseColorValue: highGlucose,
+                    lowGlucoseColorValue: lowGlucose,
+                    targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
+                    glucoseColorScheme: glucoseColorScheme,
+                    offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
+                )
+
+                RuleMark(y: .value("High", highGlucose))
+                    .foregroundStyle(highColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
-                RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(Color.loopRed)
+                RuleMark(y: .value("Low", lowGlucose))
+                    .foregroundStyle(lowColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
             }
         }
@@ -21,7 +40,10 @@ extension MainChartView {
         .chartXScale(domain: startMarker ... endMarker)
         .chartXAxis(.hidden)
         .chartYAxis { mainChartYAxis }
-        .chartYScale(domain: units == .mgdL ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
+        .chartYScale(
+            domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue.asMmolL ... state
+                .maxYAxisValue.asMmolL
+        )
         .chartLegend(.hidden)
     }
 
@@ -48,7 +70,7 @@ extension MainChartView {
         .chartXAxis(.hidden)
         .chartYAxis { cobChartYAxis }
         .chartYAxis(.hidden)
-        .chartYScale(domain: minValueCobChart ... maxValueCobChart)
+        .chartYScale(domain: state.minValueCobChart ... state.maxValueCobChart)
         .chartLegend(.hidden)
     }
 }

+ 15 - 3
FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift

@@ -7,7 +7,9 @@ struct GlucoseChartView: ChartContent {
     let units: GlucoseUnits
     let highGlucose: Decimal
     let lowGlucose: Decimal
+    let currentGlucoseTarget: Decimal
     let isSmoothingEnabled: Bool
+    let glucoseColorScheme: GlucoseColorScheme
 
     var body: some ChartContent {
         drawGlucoseChart()
@@ -16,9 +18,19 @@ struct GlucoseChartView: ChartContent {
     private func drawGlucoseChart() -> some ChartContent {
         ForEach(glucoseData) { item in
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-            let pointMarkColor: Color = glucoseToDisplay > highGlucose ? Color.orange :
-                glucoseToDisplay < lowGlucose ? Color.red :
-                Color.green
+
+            // low glucose and high glucose is parsed in state to mmol/L; parse it back to mg/dL here for comparison
+            let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+            let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+
+            let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
+                glucoseValue: Decimal(item.glucose),
+                highGlucoseColorValue: highGlucose,
+                lowGlucoseColorValue: lowGlucose,
+                targetGlucose: currentGlucoseTarget,
+                glucoseColorScheme: glucoseColorScheme,
+                offset: 20
+            )
 
             if !isSmoothingEnabled {
                 PointMark(

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

@@ -32,7 +32,7 @@ extension MainChartView {
             .backport.chartXSelection(value: $selection)
             .chartXAxis { basalChartXAxis }
             .chartYAxis { cobChartYAxis }
-            .chartYScale(domain: minValueIobChart ... maxValueIobChart)
+            .chartYScale(domain: state.minValueIobChart ... state.maxValueIobChart)
             .chartYAxis(.hidden)
         }
     }

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

@@ -11,7 +11,9 @@ struct MainChartView: View {
     @Binding var hours: Int
     @Binding var highGlucose: Decimal
     @Binding var lowGlucose: Decimal
+    @Binding var currentGlucoseTarget: Decimal
     @Binding var screenHours: Int16
+    @Binding var glucoseColorScheme: GlucoseColorScheme
     @Binding var displayXgridLines: Bool
     @Binding var displayYgridLines: Bool
     @Binding var thresholdLines: Bool
@@ -23,13 +25,9 @@ struct MainChartView: View {
     @State var startMarker =
         Date(timeIntervalSinceNow: TimeInterval(hours: -24))
     @State var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
-    @State var minValue: Decimal = 39
-    @State var maxValue: Decimal = 300
+
     @State var selection: Date? = nil
-    @State var minValueCobChart: Decimal = 0
-    @State var maxValueCobChart: Decimal = 20
-    @State var minValueIobChart: Decimal = 0
-    @State var maxValueIobChart: Decimal = 5
+
     @State var mainChartHasInitialized = false
 
     let now = Date.now
@@ -102,29 +100,22 @@ struct MainChartView: View {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
                         .onChange(of: state.glucoseFromPersistence.last?.glucose) { _ in
-                            updateStartEndMarkers()
-                            yAxisChartData()
                             scroller.scrollTo("MainChart", anchor: .trailing)
+                            updateStartEndMarkers()
                         }
                         .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) { _ in
-                            yAxisChartDataCobChart()
-                            yAxisChartDataIobChart()
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
                         .onChange(of: units) { _ in
-                            yAxisChartData()
-                            yAxisChartDataCobChart()
-                            yAxisChartDataIobChart()
+                            // TODO: - Refactor this to only update the Y Axis Scale
+                            state.setupGlucoseArray()
                         }
                         .onAppear {
                             if !mainChartHasInitialized {
+                                scroller.scrollTo("MainChart", anchor: .trailing)
                                 updateStartEndMarkers()
-                                yAxisChartData()
-                                yAxisChartDataCobChart()
-                                yAxisChartDataIobChart()
                                 calculateTempBasalsInBackground()
                                 mainChartHasInitialized = true
-                                scroller.scrollTo("MainChart", anchor: .trailing)
                             }
                         }
                     }
@@ -149,7 +140,9 @@ extension MainChartView {
                     units: state.units,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
-                    isSmoothingEnabled: state.isSmoothingEnabled
+                    currentGlucoseTarget: state.currentGlucoseTarget,
+                    isSmoothingEnabled: state.isSmoothingEnabled,
+                    glucoseColorScheme: state.glucoseColorScheme
                 )
 
                 InsulinView(
@@ -163,7 +156,7 @@ extension MainChartView {
                     units: state.units,
                     carbData: state.carbsFromPersistence,
                     fpuData: state.fpusFromPersistence,
-                    minValue: minValue
+                    minValue: state.minYAxisValue
                 )
 
                 OverrideView(
@@ -186,7 +179,7 @@ extension MainChartView {
                     minForecast: state.minForecast,
                     maxForecast: state.maxForecast,
                     units: state.units,
-                    maxValue: maxValue,
+                    maxValue: state.maxYAxisValue,
                     forecastDisplayType: state.forecastDisplayType
                 )
 
@@ -241,7 +234,10 @@ extension MainChartView {
             .chartYAxis { mainChartYAxis }
             .chartYAxis(.hidden)
             .backport.chartXSelection(value: $selection)
-            .chartYScale(domain: units == .mgdL ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
+            .chartYScale(
+                domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue
+                    .asMmolL ... state.maxYAxisValue.asMmolL
+            )
             .backport.chartForegroundStyleScale(state: state)
         }
     }
@@ -256,13 +252,20 @@ extension MainChartView {
                         .font(.body).bold()
                 }.font(.body).padding(.bottom, 5)
 
+                let glucoseColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: glucoseToShow,
+                    highGlucoseColorValue: highGlucose,
+                    lowGlucoseColorValue: lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme,
+                    offset: units == .mgdL ? 20 : 20.asMmolL
+                )
                 HStack {
                     Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
                         .bold()
                         + Text(" \(units.rawValue)")
                 }.foregroundStyle(
-                    glucoseToShow < lowGlucose ? Color
-                        .red : (glucoseToShow > highGlucose ? Color.orange : Color.primary)
+                    Color(glucoseColor)
                 ).font(.body)
 
                 if let selectedIOBValue, let iob = selectedIOBValue.iob {
@@ -411,84 +414,6 @@ extension MainChartView {
             .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
             dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
     }
-
-    private func yAxisChartData() {
-        Task {
-            let (minGlucose, maxGlucose, minForecast, maxForecast) = await Task
-                .detached { () -> (Decimal?, Decimal?, Decimal?, Decimal?) in
-                    let glucoseMapped = await state.glucoseFromPersistence.map { Decimal($0.glucose) }
-                    let forecastValues = await state.preprocessedData.map { Decimal($0.forecastValue.value) }
-
-                    // Calculate min and max values for glucose and forecast
-                    return (glucoseMapped.min(), glucoseMapped.max(), forecastValues.min(), forecastValues.max())
-                }.value
-
-            // Ensure all values exist, otherwise set default values
-            guard let minGlucose = minGlucose, let maxGlucose = maxGlucose,
-                  let minForecast = minForecast, let maxForecast = maxForecast
-            else {
-                await updateChartBounds(minValue: 39, maxValue: 300)
-                return
-            }
-
-            // Adjust max forecast to be no more than 100 over max glucose
-            let adjustedMaxForecast = min(maxForecast, maxGlucose + 100)
-            let minOverall = min(minGlucose, minForecast)
-            let maxOverall = max(maxGlucose, adjustedMaxForecast)
-
-            // Update the chart bounds on the main thread
-            await updateChartBounds(minValue: minOverall - 50, maxValue: maxOverall + 80)
-        }
-    }
-
-    @MainActor private func updateChartBounds(minValue: Decimal, maxValue: Decimal) async {
-        self.minValue = minValue
-        self.maxValue = maxValue
-    }
-
-    private func yAxisChartDataCobChart() {
-        Task {
-            let maxCob = await Task.detached { () -> Decimal? in
-                let cobMapped = await state.enactedAndNonEnactedDeterminations.map { Decimal($0.cob) }
-                return cobMapped.max()
-            }.value
-
-            // Ensure the result exists or set default values
-            if let maxCob = maxCob {
-                let calculatedMax = maxCob == 0 ? 20 : maxCob + 20
-                await updateCobChartBounds(minValue: 0, maxValue: calculatedMax)
-            } else {
-                await updateCobChartBounds(minValue: 0, maxValue: 20)
-            }
-        }
-    }
-
-    @MainActor private func updateCobChartBounds(minValue: Decimal, maxValue: Decimal) async {
-        minValueCobChart = minValue
-        maxValueCobChart = maxValue
-    }
-
-    private func yAxisChartDataIobChart() {
-        Task {
-            let (minIob, maxIob) = await Task.detached { () -> (Decimal?, Decimal?) in
-                let iobMapped = await state.enactedAndNonEnactedDeterminations.compactMap { $0.iob?.decimalValue }
-                return (iobMapped.min(), iobMapped.max())
-            }.value
-
-            // Ensure min and max IOB values exist, or set defaults
-            if let minIob = minIob, let maxIob = maxIob {
-                let adjustedMin = minIob < 0 ? minIob - 2 : 0
-                await updateIobChartBounds(minValue: adjustedMin, maxValue: maxIob + 2)
-            } else {
-                await updateIobChartBounds(minValue: 0, maxValue: 5)
-            }
-        }
-    }
-
-    @MainActor private func updateIobChartBounds(minValue: Decimal, maxValue: Decimal) async {
-        minValueIobChart = minValue
-        maxValueIobChart = maxValue
-    }
 }
 
 extension Int16 {

+ 50 - 36
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -8,6 +8,8 @@ struct CurrentGlucoseView: View {
     @Binding var lowGlucose: Decimal
     @Binding var highGlucose: Decimal
     @Binding var cgmAvailable: Bool
+    @Binding var currentGlucoseTarget: Decimal
+    @Binding var glucoseColorScheme: GlucoseColorScheme
 
     var glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
 
@@ -79,15 +81,34 @@ struct CurrentGlucoseView: View {
                         if let glucoseValue = glucose.last?.glucose {
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                                 .formattedAsMmolL
-                            Text(
+
+                            // low glucose, high glucose and target is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+                            let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+                            let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+                            let targetGlucose = units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMgdL
+
+                            var glucoseDisplayColor = Color.primary
+
+                            if Decimal(glucoseValue) <= lowGlucose || Decimal(glucoseValue) >= highGlucose {
+                                glucoseDisplayColor = FreeAPS.getDynamicGlucoseColor(
+                                    glucoseValue: Decimal(glucoseValue),
+                                    highGlucoseColorValue: highGlucose,
+                                    lowGlucoseColorValue: lowGlucose,
+                                    targetGlucose: targetGlucose,
+                                    glucoseColorScheme: glucoseColorScheme,
+                                    offset: 20
+                                )
+                            }
+
+                            return Text(
                                 glucoseValue == 400 ? "HIGH" : displayGlucose
                             )
                             .font(.system(size: 40, weight: .bold, design: .rounded))
-                            .foregroundColor(alarm == nil ? glucoseDisplayColor : .loopRed)
+                            .foregroundStyle(glucoseDisplayColor)
                         } else {
-                            Text("--")
+                            return Text("--")
                                 .font(.system(size: 40, weight: .bold, design: .rounded))
-                                .foregroundColor(.secondary)
+                                .foregroundStyle(.secondary)
                         }
                     }
                     HStack {
@@ -99,18 +120,18 @@ struct CurrentGlucoseView: View {
                                     NSLocalizedString("min", comment: "Short form for minutes") + " "
                             )
                         )
-                        .font(.caption2).foregroundColor(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
+                        .font(.caption2).foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
 
                         Text(
                             delta
                         )
-                        .font(.caption2).foregroundColor(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
+                        .font(.caption2).foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
                     }.frame(alignment: .top)
                 }
             }
-            .onChange(of: glucose.last?.directionEnum) { newDirection in
+            .onChange(of: glucose.last?.directionEnum) {
                 withAnimation {
-                    switch newDirection {
+                    switch glucose.last?.directionEnum {
                     case .doubleUp,
                          .singleUp,
                          .tripleUp:
@@ -160,34 +181,27 @@ struct CurrentGlucoseView: View {
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
 
-    var glucoseDisplayColor: Color {
-        guard let lastGlucose = glucose.last?.glucose else { return .primary }
-
-        // Convert the lastest glucose value to Int for comparison
-        let whichGlucose = Int(lastGlucose)
-
-        // Define default color based on the color scheme
-        let defaultColor: Color = colorScheme == .dark ? .white : .black
-
-        // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
-        let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
-        let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
-
-        // Ensure the thresholds are logical
-        guard lowGlucose < highGlucose else { return .primary }
-
-        // Perform range checks using Int converted values
-        switch whichGlucose {
-        case 0 ..< Int(lowGlucose):
-            return .loopRed
-        case Int(lowGlucose) ..< Int(highGlucose):
-            return defaultColor
-        case Int(highGlucose)...:
-            return .loopYellow
-        default:
-            return defaultColor
-        }
-    }
+//    var glucoseDisplayColor: Color {
+//        guard let lastGlucose = glucose.last?.glucose else { return .primary }
+//
+//        // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+//        let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+//        let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+//
+//        // Ensure the thresholds are logical
+//        guard lowGlucose < highGlucose else { return .primary }
+//
+//        guard Decimal(lastGlucose) <= lowGlucose && Decimal(lastGlucose) >= highGlucose else { return .primary }
+//
+//        return FreeAPS.getDynamicGlucoseColor(
+//            glucoseValue: Decimal(lastGlucose),
+//            highGlucoseColorValue: highGlucose,
+//            lowGlucoseColorValue: lowGlucose,
+//            targetGlucose: currentGlucoseTarget,
+//            glucoseColorScheme: glucoseColorScheme,
+//            offset: 20
+//        )
+//    }
 }
 
 struct Triangle: Shape {

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

@@ -132,6 +132,8 @@ extension Home {
                 lowGlucose: $state.lowGlucose,
                 highGlucose: $state.highGlucose,
                 cgmAvailable: $state.cgmAvailable,
+                currentGlucoseTarget: $state.currentGlucoseTarget,
+                glucoseColorScheme: $state.glucoseColorScheme,
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
                 .onTapGesture {
@@ -354,7 +356,9 @@ extension Home {
                     hours: .constant(state.filteredHours),
                     highGlucose: $state.highGlucose,
                     lowGlucose: $state.lowGlucose,
+                    currentGlucoseTarget: $state.currentGlucoseTarget,
                     screenHours: $state.hours,
+                    glucoseColorScheme: $state.glucoseColorScheme,
                     displayXgridLines: $state.displayXgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     thresholdLines: $state.thresholdLines,

+ 7 - 2
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -326,12 +326,17 @@ extension NightscoutConfig {
             if glucose.isNotEmpty {
                 await MainActor.run {
                     self.backfilling = false
-                    self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
-                    self.glucoseStorage.storeGlucose(glucose)
+                }
+
+                glucoseStorage.storeGlucose(glucose)
+
+                Task.detached {
+                    await self.healthKitManager.uploadGlucose()
                 }
             } else {
                 await MainActor.run {
                     self.backfilling = false
+                    debug(.nightscout, "No glucose values found or fetched to backfill.")
                 }
             }
         }

+ 14 - 1
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -41,7 +41,20 @@ extension NightscoutConfig {
                     Section(
                         header: Text("Nightscout Integration"),
                         content: {
-                            NavigationLink("Connect", destination: NightscoutConnectView(state: state))
+                            NavigationLink(destination: NightscoutConnectView(state: state), label: {
+                                HStack {
+                                    Text("Connect")
+                                    ZStack {
+                                        if state.isConnectedToNS {
+                                            Image(systemName: "network")
+                                            Image(systemName: "checkmark.circle.fill").foregroundColor(.green).font(.caption2)
+                                                .offset(x: 9, y: 6)
+                                        } else {
+                                            Image(systemName: "network.slash")
+                                        }
+                                    }
+                                }
+                            })
                             NavigationLink("Upload", destination: NightscoutUploadView(state: state))
                             NavigationLink("Fetch & Remote Control", destination: NightscoutFetchView(state: state))
                         }

+ 3 - 1
FreeAPS/Sources/Modules/Settings/SettingItems.swift

@@ -191,7 +191,9 @@ enum SettingItems {
                 "Standing / Laying TIR Chart",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
-                "Forecast Display Type"
+                "Forecast Display Type",
+                "Trio Color Scheme",
+                "Glucose Color Scheme"
             ],
             path: ["Features", "User Interface"]
         ),

+ 1 - 1
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -99,6 +99,6 @@ extension Settings.StateModel: ServiceOnboardingDelegate {
 extension Settings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
         setupTidepool = false
-        provider.tidepoolManager.forceUploadData(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
+        provider.tidepoolManager.forceTidepoolDataUpload()
     }
 }

+ 29 - 5
FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift

@@ -36,12 +36,36 @@ struct TidepoolStartView: BaseView {
                 content:
                 {
                     VStack {
-                        Button {
-                            state.setupTidepool.toggle()
+                        if let serviceUIType = state.serviceUIType,
+                           let pluginHost = state.provider.tidepoolManager.getTidepoolPluginHost()
+                        {
+                            if let serviceUI = state.provider.tidepoolManager.getTidepoolServiceUI()
+                            {
+                                Button {
+                                    state.setupTidepool.toggle()
+                                }
+                                label: {
+                                    HStack {
+                                        Text("Connected to Tidepool").font(.title3)
+                                        ZStack {
+                                            Image(systemName: "network")
+                                            Image(systemName: "checkmark.circle.fill")
+                                                .foregroundColor(.green).font(.caption2)
+                                                .offset(x: 9, y: 6)
+                                        }
+                                    }
+                                }
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .buttonStyle(.bordered)
+                            } else {
+                                Button {
+                                    state.setupTidepool.toggle()
+                                }
+                                label: { Text("Connect to Tidepool").font(.title3) }
+                                    .frame(maxWidth: .infinity, alignment: .center)
+                                    .buttonStyle(.bordered)
+                            }
                         }
-                        label: { Text("Connect to Tidepool").font(.title3) }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .buttonStyle(.bordered)
 
                         HStack(alignment: .top) {
                             Text("You can connect Trio to seamlessly upload and manage your diabetes data on Tidepool.")

+ 2 - 2
FreeAPS/Sources/Modules/Stat/StatStateModel.swift

@@ -70,9 +70,9 @@ extension Stat {
                 propertiesToFetch: ["glucose", "objectID"]
             )
 
-            guard let fetchedResults = results as? [[String: Any]] else { return [] }
-
             return await context.perform {
+                guard let fetchedResults = results as? [[String: Any]] else { return [] }
+
                 return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
             }
         }

+ 2 - 0
FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -12,6 +12,7 @@ extension StatConfig {
         @Published var low: Decimal = 70
         @Published var high: Decimal = 180
         @Published var hours: Decimal = 6
+        @Published var dynamicGlucoseColor = false
         @Published var xGridLines = false
         @Published var yGridLines: Bool = false
         @Published var oneDimensionalGraph = false
@@ -25,6 +26,7 @@ extension StatConfig {
             self.units = units
 
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
+            subscribeSetting(\.dynamicGlucoseColor, on: $dynamicGlucoseColor) { dynamicGlucoseColor = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }

+ 1 - 0
FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -45,6 +45,7 @@ extension StatConfig {
         var body: some View {
             Form {
                 Section {
+                    Toggle("Use Dynamic BG Color", isOn: $state.dynamicGlucoseColor)
                     Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)

+ 3 - 0
FreeAPS/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -14,6 +14,7 @@ extension UserInterfaceSettings {
         @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
 
         var units: GlucoseUnits = .mgdL
 
@@ -41,6 +42,8 @@ extension UserInterfaceSettings {
                 \.carbsRequiredThreshold,
                 on: $carbsRequiredThreshold
             ) { carbsRequiredThreshold = $0 }
+
+            subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
         }
     }
 }

+ 37 - 1
FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -61,7 +61,7 @@ extension UserInterfaceSettings {
                         VStack {
                             Picker(
                                 selection: $colorSchemePreference,
-                                label: Text("Color Scheme")
+                                label: Text("Trio Color Scheme")
                             ) {
                                 ForEach(ColorSchemeOption.allCases) { selection in
                                     Text(selection.displayName).tag(selection)
@@ -93,6 +93,42 @@ extension UserInterfaceSettings {
                     }
                 ).listRowBackground(Color.chart)
 
+                Section {
+                    VStack {
+                        Picker(
+                            selection: $state.glucoseColorScheme,
+                            label: Text("Glucose Color Scheme")
+                        ) {
+                            ForEach(GlucoseColorScheme.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+
+                        HStack(alignment: .top) {
+                            Text(
+                                "Glucose Color Scheme ... dynamic or static ... Lorem ipsum dolor"
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = "Glucose Color Scheme"
+                                    selectedVerboseHint =
+                                        "Glucose Color Scheme... Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
+
                 Section(
                     header: Text("Home View Settings"),
                     content: {

+ 4 - 4
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -183,9 +183,9 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
         )
 
-        guard let fetchedResults = results as? [[String: Any]] /* , !fetchedResults.isEmpty */ else { return nil }
-
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else { return nil }
+
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
         }
     }
@@ -200,9 +200,9 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["objectID", "glucose"]
         )
 
-        guard let fetchedResults = results as? [[String: Any]] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else { return [] }
+
             return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
         }
     }

Plik diff jest za duży
+ 500 - 487
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift


+ 12 - 12
FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -14,11 +14,11 @@ extension LiveActivityBridge {
             fetchLimit: 72
         )
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return
-        }
-
         await context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return
+            }
+
             self.glucoseFromPersistence = glucoseResults.map {
                 GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum)
             }
@@ -36,11 +36,11 @@ extension LiveActivityBridge {
             propertiesToFetch: ["iob", "cob"]
         )
 
-        guard let determinationResults = results as? [[String: Any]] else {
-            return
-        }
-
         await context.perform {
+            guard let determinationResults = results as? [[String: Any]] else {
+                return
+            }
+
             self.determination = determinationResults.first.map {
                 DeterminationData(
                     cob: ($0["cob"] as? Int) ?? 0,
@@ -61,11 +61,11 @@ extension LiveActivityBridge {
             propertiesToFetch: ["enabled"]
         )
 
-        guard let overrideResults = results as? [[String: Any]] else {
-            return
-        }
-
         await context.perform {
+            guard let overrideResults = results as? [[String: Any]] else {
+                return
+            }
+
             self.isOverridesActive = overrideResults.first.map {
                 OverrideData(isActive: $0["enabled"] as? Bool ?? false)
             }

+ 3 - 3
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -7,7 +7,9 @@ struct LiveActivityAttributes: ActivityAttributes {
         let direction: String?
         let change: String
         let date: Date
-
+        let highGlucose: Decimal
+        let lowGlucose: Decimal
+        let glucoseColorScheme: String
         let detailedViewState: ContentAdditionalState?
 
         /// true for the first state that is set on the activity
@@ -18,8 +20,6 @@ struct LiveActivityAttributes: ActivityAttributes {
         let chart: [Decimal]
         let chartDate: [Date?]
         let rotationDegrees: Double
-        let highGlucose: Decimal
-        let lowGlucose: Decimal
         let cob: Decimal
         let iob: Decimal
         let unit: String

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

@@ -87,8 +87,6 @@ extension LiveActivityAttributes.ContentState {
                 chart: chartBG,
                 chartDate: chartDate,
                 rotationDegrees: rotationDegrees,
-                highGlucose: settings.high,
-                lowGlucose: settings.low,
                 cob: Decimal(determination?.cob ?? 0),
                 iob: determination?.iob ?? 0 as Decimal,
                 unit: settings.units.rawValue,
@@ -104,6 +102,9 @@ extension LiveActivityAttributes.ContentState {
             direction: trendString,
             change: change,
             date: bg.date,
+            highGlucose: settings.high,
+            lowGlucose: settings.low,
+            glucoseColorScheme: settings.glucoseColorScheme.rawValue,
             detailedViewState: detailedState,
             isInitialState: false
         )

+ 3 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -192,6 +192,9 @@ import UIKit
                         direction: nil,
                         change: "--",
                         date: Date.now,
+                        highGlucose: settings.high,
+                        lowGlucose: settings.low,
+                        glucoseColorScheme: settings.glucoseColorScheme.rawValue,
                         detailedViewState: nil,
                         isInitialState: true
                     ),

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

@@ -85,6 +85,16 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 .share()
                 .eraseToAnyPublisher()
 
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+
         registerHandlers()
         setupNotification()
     }
@@ -395,7 +405,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
         }
 
-        if var fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
+        if let fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
             var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
             modifiedFetchedEnactedDetermination?
                 .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)

+ 474 - 245
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -1,4 +1,5 @@
 import Combine
+import CoreData
 import Foundation
 import HealthKit
 import LoopKit
@@ -9,13 +10,12 @@ protocol TidepoolManager {
     func addTidepoolService(service: Service)
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolPluginHost() -> PluginHost?
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
-    func deleteInsulin(at date: Date)
-//    func uploadStatus()
-    func uploadGlucose(device: HKDevice?) async
-    func forceUploadData(device: HKDevice?)
-//    func uploadPreferences(_ preferences: Preferences)
-//    func uploadProfileAndSettings(_: Bool)
+    func uploadCarbs() async
+    func deleteCarbs(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String)
+    func uploadInsulin() async
+    func deleteInsulin(withSyncId id: String, amount: Decimal, at: Date)
+    func uploadGlucose() async
+    func forceTidepoolDataUpload()
 }
 
 final class BaseTidepoolManager: TidepoolManager, Injectable {
@@ -25,6 +25,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
+    @Injected() private var apsManager: APSManager!
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var tidepoolService: RemoteDataService? {
@@ -37,15 +38,39 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
+    private var backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
+
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
     init(resolver: Resolver) {
         injectServices(resolver)
         loadTidepoolManager()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+
+        registerHandlers()
+
         subscribe()
     }
 
-    /// load the Tidepool Remote Data Service if available
+    /// Loads the Tidepool service from saved state
     fileprivate func loadTidepoolManager() {
         if let rawTidepoolManager = rawTidepoolManager {
             tidepoolService = tidepoolServiceFromRaw(rawTidepoolManager)
@@ -54,39 +79,62 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
-    /// allows access to tidepoolService as a simple ServiceUI
+    /// Returns the Tidepool service UI if available
     func getTidepoolServiceUI() -> ServiceUI? {
-        if let tidepoolService = self.tidepoolService {
-            return tidepoolService as! any ServiceUI as ServiceUI
-        } else {
-            return nil
-        }
+        tidepoolService as? ServiceUI
     }
 
-    /// get the pluginHost of Tidepool
+    /// Returns the Tidepool plugin host
     func getTidepoolPluginHost() -> PluginHost? {
         self as PluginHost
     }
 
+    /// Adds a Tidepool service
     func addTidepoolService(service: Service) {
-        tidepoolService = service as! any RemoteDataService as RemoteDataService
+        tidepoolService = service as? RemoteDataService
     }
 
-    /// load the Tidepool Remote Data Service from raw storage
+    /// Loads the Tidepool service from raw stored data
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
-        else {
-            return nil
-        }
+        else { return nil }
+
         if let service = serviceType.init(rawState: rawState) {
-            return service as! any RemoteDataService as RemoteDataService
-        } else { return nil }
+            return service as? RemoteDataService
+        }
+        return nil
+    }
+
+    /// Registers handlers for Core Data changes
+    private func registerHandlers() {
+        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadInsulin()
+            }
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadCarbs()
+            }
+        }.store(in: &subscriptions)
+
+        // This works only for manual Glucose
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadGlucose()
+            }
+        }.store(in: &subscriptions)
     }
 
     private func subscribe() {
-        broadcaster.register(PumpHistoryObserver.self, observer: self)
-        broadcaster.register(CarbsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
     }
 
@@ -94,9 +142,63 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         nil
     }
 
-    func uploadCarbs() {
-        let carbs: [CarbsEntry] = carbsStorage.recent()
+    /// Forces a full data upload to Tidepool
+    func forceTidepoolDataUpload() {
+        Task {
+            await uploadInsulin()
+            await uploadCarbs()
+            await uploadGlucose()
+        }
+    }
+}
+
+extension BaseTidepoolManager: TempTargetsObserver {
+    func tempTargetsDidUpdate(_: [TempTarget]) {}
+}
+
+extension BaseTidepoolManager: ServiceDelegate {
+    var hostIdentifier: String {
+        // TODO: shouldn't this rather be `org.nightscout.Trio` ?
+        "com.loopkit.Loop" // To check
+    }
+
+    var hostVersion: String {
+        var semanticVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
+
+        while semanticVersion.split(separator: ".").count < 3 {
+            semanticVersion += ".0"
+        }
+
+        semanticVersion += "+\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String)"
+
+        return semanticVersion
+    }
+
+    func issueAlert(_: LoopKit.Alert) {}
+
+    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
+
+    func enactRemoteOverride(name _: String, durationTime _: TimeInterval?, remoteAddress _: String) async throws {}
+
+    func cancelRemoteOverride() async throws {}
 
+    func deliverRemoteCarbs(
+        amountInGrams _: Double,
+        absorptionTime _: TimeInterval?,
+        foodType _: String?,
+        startDate _: Date?
+    ) async throws {}
+
+    func deliverRemoteBolus(amountInUnits _: Double) async throws {}
+}
+
+/// Carb Upload and Deletion Functionality
+extension BaseTidepoolManager {
+    func uploadCarbs() async {
+        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
+    }
+
+    func uploadCarbs(_ carbs: [CarbsEntry]) {
         guard !carbs.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         processQueue.async {
@@ -108,62 +210,223 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                     switch result {
                     case let .failure(error):
-                        debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                        debug(.nightscout, "Error synchronizing carbs data with Tidepool: \(String(describing: error))")
                     case .success:
-                        debug(.nightscout, "Success synchronizing carbs data:")
+                        debug(.nightscout, "Success synchronizing carbs data. Upload to Tidepool complete.")
+                        // After successful upload, update the isUploadedToTidepool flag in Core Data
+                        Task {
+                            await self.updateCarbsAsUploaded(carbs)
+                        }
                     }
                 }
             }
         }
     }
 
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID _: String) {
-        guard let tidepoolService = self.tidepoolService else { return }
+    private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
+        await backgroundContext.perform {
+            let ids = carbs.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
-        processQueue.async {
-            var carbsToDelete: [CarbsEntry] = []
-            let allValues = self.storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToTidepool = true
+                }
 
-            if let isFPU = isFPU, isFPU {
-                guard let fpuID = fpuID else { return }
-                carbsToDelete = allValues.filter { $0.fpuID == fpuID }.removeDublicates()
-            } else {
-                carbsToDelete = allValues.filter { $0.createdAt == date }.removeDublicates()
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
+                )
             }
+        }
+    }
 
-            let syncCarb = carbsToDelete.map { d in
-                d.convertSyncCarb(operation: .delete)
-            }
+    func deleteCarbs(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String) {
+        guard let tidepoolService = self.tidepoolService else { return }
+
+        processQueue.async {
+            let syncCarb: [SyncCarbObject] = [SyncCarbObject(
+                absorptionTime: nil,
+                createdByCurrentApp: true,
+                foodType: nil,
+                grams: Double(carbs),
+                startDate: at,
+                uuid: id,
+                provenanceIdentifier: enteredBy,
+                syncIdentifier: id.uuidString,
+                syncVersion: nil,
+                userCreatedDate: nil,
+                userUpdatedDate: nil,
+                userDeletedDate: nil,
+                operation: LoopKit.Operation.delete,
+                addedDate: nil,
+                supercededDate: nil
+            )]
 
             tidepoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
                 switch result {
                 case let .failure(error):
-                    debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                    debug(.nightscout, "Error synchronizing carbs data with Tidepool: \(String(describing: error))")
                 case .success:
-                    debug(.nightscout, "Success synchronizing carbs data:")
+                    debug(.nightscout, "Success synchronizing carbs data. Upload to Tidepool complete.")
                 }
             }
         }
     }
+}
 
-    func deleteInsulin(at d: Date) {
-        let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
+/// Insulin Upload and Deletion Functionality
+extension BaseTidepoolManager {
+    func uploadInsulin() async {
+        await uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
+    }
 
-        guard !allValues.isEmpty, let tidepoolService = self.tidepoolService else { return }
+    func uploadDose(_ events: [PumpHistoryEvent]) async {
+        guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
+
+        // Fetch all temp basal entries from Core Data for the last 24 hours
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: backgroundContext,
+            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                NSPredicate.pumpHistoryLast24h,
+                NSPredicate(format: "tempBasal != nil")
+            ]),
+            key: "timestamp",
+            ascending: true,
+            batchSize: 50
+        )
+
+        // Ensure that the processing happens within the background context for thread safety
+        await backgroundContext.perform {
+            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
+
+            let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
+                var result = result
+                switch event.type {
+                case .tempBasal:
+                    result
+                        .append(contentsOf: self.processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries))
+                case .bolus:
+                    let bolusDoseEntry = DoseEntry(
+                        type: .bolus,
+                        startDate: event.timestamp,
+                        endDate: event.timestamp,
+                        value: Double(event.amount!),
+                        unit: .units,
+                        deliveredUnits: nil,
+                        syncIdentifier: event.id,
+                        scheduledBasalRate: nil,
+                        insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                        automatic: event.isSMB ?? true,
+                        manuallyEntered: event.isExternal ?? false
+                    )
+                    result.append(bolusDoseEntry)
+                default:
+                    break
+                }
+                return result
+            }
+
+            debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
+
+            let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
+                if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
+                    let dose: DoseEntry? = switch pumpEventType {
+                    case .suspend:
+                        DoseEntry(suspendDate: event.timestamp, automatic: true)
+                    case .resume:
+                        DoseEntry(resumeDate: event.timestamp, automatic: true)
+                    default:
+                        nil
+                    }
+
+                    return PersistedPumpEvent(
+                        date: event.timestamp,
+                        persistedDate: event.timestamp,
+                        dose: dose,
+                        isUploaded: true,
+                        objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
+                        raw: event.id.data(using: .utf8),
+                        title: event.note,
+                        type: pumpEventType
+                    )
+                } else {
+                    return nil
+                }
+            }
+
+            self.processQueue.async {
+                tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
+                    switch result {
+                    case let .failure(error):
+                        debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
+                    case .success:
+                        debug(.nightscout, "Success synchronizing dose data. Upload to Tidepool complete.")
+                        Task {
+                            let insulinEvents = events.filter {
+                                $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+                            }
+                            await self.updateInsulinAsUploaded(insulinEvents)
+                        }
+                    }
+                }
 
-        var doseDataToDelete: [DoseEntry] = []
+                tidepoolService.uploadPumpEventData(pumpEvents) { result in
+                    switch result {
+                    case let .failure(error):
+                        debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
+                    case .success:
+                        debug(.nightscout, "Success synchronizing pump events data. Upload to Tidepool complete.")
+                        Task {
+                            let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
+                            let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
 
-        guard let entry = allValues.first(where: { $0.timestamp == d }) else {
-            return
+                            await self.updateInsulinAsUploaded(pumpEvents)
+                        }
+                    }
+                }
+            }
         }
-        doseDataToDelete
-            .append(DoseEntry(
-                type: .bolus,
-                startDate: entry.timestamp,
-                value: Double(entry.amount!),
-                unit: .units,
-                syncIdentifier: entry.id
-            ))
+    }
+
+    private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
+        await backgroundContext.perform {
+            let ids = insulin.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToTidepool = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    func deleteInsulin(withSyncId id: String, amount: Decimal, at: Date) {
+        guard let tidepoolService = self.tidepoolService else { return }
+
+        // must be an array here, because `tidepoolService.uploadDoseData` expects a `deleted` array
+        let doseDataToDelete: [DoseEntry] = [DoseEntry(
+            type: .bolus,
+            startDate: at,
+            value: Double(amount),
+            unit: .units,
+            syncIdentifier: id
+        )]
 
         processQueue.async {
             tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
@@ -171,239 +434,205 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
                 case .success:
-                    debug(.nightscout, "Success synchronizing Dose delete data:")
+                    debug(.nightscout, "Success synchronizing Dose delete data")
                 }
             }
         }
     }
+}
 
-    func uploadDose() {
-        let events = pumpHistoryStorage.recent()
-        guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
-
-        let eventsBasal = events.filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
-            .sorted { $0.timestamp < $1.timestamp }
-
-        let doseDataBasal: [DoseEntry] = eventsBasal.reduce([]) { result, event in
-            var result = result
-            switch event.type {
-            case .tempBasal:
-                // update the previous tempBasal with endtime = starttime of the last event
-                if let last: DoseEntry = result.popLast() {
-                    let value = max(
-                        0,
-                        Double(event.timestamp.timeIntervalSince1970 - last.startDate.timeIntervalSince1970) / 3600
-                    ) *
-                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
-                    result.append(DoseEntry(
-                        type: .tempBasal,
-                        startDate: last.startDate,
-                        endDate: event.timestamp,
-                        value: value,
-                        unit: last.unit,
-                        deliveredUnits: value,
-                        syncIdentifier: last.syncIdentifier,
-                        // scheduledBasalRate: last.scheduledBasalRate,
-                        insulinType: last.insulinType,
-                        automatic: last.automatic,
-                        manuallyEntered: last.manuallyEntered
-                    ))
+/// Insulin Helper Functions
+extension BaseTidepoolManager {
+    private func processTempBasalEvent(
+        _ event: PumpHistoryEvent,
+        existingTempBasalEntries: [PumpEventStored]
+    ) -> [DoseEntry] {
+        var insulinDoseEvents: [DoseEntry] = []
+
+        backgroundContext.performAndWait {
+            // Loop through the pump history events within the background context
+            guard let duration = event.duration, let amount = event.amount,
+                  let currentBasalRate = self.getCurrentBasalRate()
+            else {
+                return
+            }
+            let value = (Decimal(duration) / 60.0) * amount
+
+            // Find the corresponding temp basal entry in existingTempBasalEntries
+            if let matchingEntryIndex = existingTempBasalEntries.firstIndex(where: { $0.timestamp == event.timestamp }) {
+                // Check for a predecessor (the entry before the matching entry)
+                let predecessorIndex = matchingEntryIndex - 1
+                if predecessorIndex >= 0 {
+                    let predecessorEntry = existingTempBasalEntries[predecessorIndex]
+                    if let predecessorTimestamp = predecessorEntry.timestamp,
+                       let predecessorEntrySyncIdentifier = predecessorEntry.id
+                    {
+                        let predecessorEndDate = predecessorTimestamp
+                            .addingTimeInterval(TimeInterval(
+                                Int(predecessorEntry.tempBasal?.duration ?? 0) *
+                                    60
+                            )) // parse duration to minutes
+
+                        // If the predecessor's end date is later than the current event's start date, adjust it
+                        if predecessorEndDate > event.timestamp {
+                            let adjustedEndDate = event.timestamp
+                            let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
+                            let adjustedDeliveredUnits = (adjustedDuration / 3600) *
+                                Double(truncating: predecessorEntry.tempBasal?.rate ?? 0)
+
+                            // Create updated predecessor dose entry
+                            let updatedPredecessorEntry = DoseEntry(
+                                type: .tempBasal,
+                                startDate: predecessorTimestamp,
+                                endDate: adjustedEndDate,
+                                value: adjustedDeliveredUnits,
+                                unit: .units,
+                                deliveredUnits: adjustedDeliveredUnits,
+                                syncIdentifier: predecessorEntrySyncIdentifier,
+                                insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                                automatic: true,
+                                manuallyEntered: false,
+                                isMutable: false
+                            )
+                            // Add the updated predecessor entry to the result
+                            insulinDoseEvents.append(updatedPredecessorEntry)
+                        }
+                    }
                 }
-                result.append(DoseEntry(
+
+                // Create a new dose entry for the current event
+                let currentEndDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration)))
+                let newDoseEntry = DoseEntry(
                     type: .tempBasal,
                     startDate: event.timestamp,
-                    value: 0.0,
+                    endDate: currentEndDate,
+                    value: Double(value),
                     unit: .units,
+                    deliveredUnits: Double(value),
                     syncIdentifier: event.id,
-                    scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(event.rate!)),
-                    insulinType: nil,
+                    scheduledBasalRate: HKQuantity(
+                        unit: .internationalUnitsPerHour,
+                        doubleValue: Double(currentBasalRate.rate)
+                    ),
+                    insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
                     automatic: true,
                     manuallyEntered: false,
-                    isMutable: true
-                ))
-            case .tempBasalDuration:
-                if let last: DoseEntry = result.popLast(),
-                   last.type == .tempBasal,
-                   last.startDate == event.timestamp
-                {
-                    let durationMin = event.durationMin ?? 0
-                    // result.append(last)
-                    let value = (Double(durationMin) / 60.0) *
-                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
-                    result.append(DoseEntry(
-                        type: .tempBasal,
-                        startDate: last.startDate,
-                        endDate: Calendar.current.date(byAdding: .minute, value: durationMin, to: last.startDate) ?? last
-                            .startDate,
-                        value: value,
-                        unit: last.unit,
-                        deliveredUnits: value,
-                        syncIdentifier: last.syncIdentifier,
-                        scheduledBasalRate: last.scheduledBasalRate,
-                        insulinType: last.insulinType,
-                        automatic: last.automatic,
-                        manuallyEntered: last.manuallyEntered
-                    ))
-                }
-            default: break
-            }
-            return result
-        }
-
-        let boluses: [DoseEntry] = events.compactMap { event -> DoseEntry? in
-            switch event.type {
-            case .bolus:
-                return DoseEntry(
-                    type: .bolus,
-                    startDate: event.timestamp,
-                    endDate: event.timestamp,
-                    value: Double(event.amount!),
-                    unit: .units,
-                    deliveredUnits: nil,
-                    syncIdentifier: event.id,
-                    scheduledBasalRate: nil,
-                    insulinType: nil,
-                    automatic: true,
-                    manuallyEntered: false
+                    isMutable: false
                 )
-            default: return nil
+                // Add the new event entry to the result
+                insulinDoseEvents.append(newDoseEntry)
             }
         }
 
-        let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
-            if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
-                let dose: DoseEntry? = switch pumpEventType {
-                case .suspend:
-                    DoseEntry(suspendDate: event.timestamp, automatic: true)
-                case .resume:
-                    DoseEntry(resumeDate: event.timestamp, automatic: true)
-                default:
-                    nil
-                }
+        return insulinDoseEvents
+    }
 
-                return PersistedPumpEvent(
-                    date: event.timestamp,
-                    persistedDate: event.timestamp,
-                    dose: dose,
-                    isUploaded: true,
-                    objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
-                    raw: event.id.data(using: .utf8),
-                    title: event.note,
-                    type: pumpEventType
-                )
-            } else {
-                return nil
+    private func getCurrentBasalRate() -> BasalProfileEntry? {
+        let now = Date()
+        let calendar = Calendar.current
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "HH:mm:ss"
+        dateFormatter.timeZone = TimeZone.current
+
+        let basalEntries = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+            ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
+            ?? []
+
+        var currentRate: BasalProfileEntry = basalEntries[0]
+
+        for (index, entry) in basalEntries.enumerated() {
+            guard let entryTime = dateFormatter.date(from: entry.start) else {
+                print("Invalid entry start time: \(entry.start)")
+                continue
             }
-        }
 
-        processQueue.async {
-            tidepoolService.uploadDoseData(created: doseDataBasal + boluses, deleted: []) { result in
-                switch result {
-                case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
-                case .success:
-                    debug(.nightscout, "Success synchronizing Dose data:")
-                }
+            let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+            let entryStartTime = calendar.date(
+                bySettingHour: entryComponents.hour!,
+                minute: entryComponents.minute!,
+                second: entryComponents.second!,
+                of: now
+            )!
+
+            let entryEndTime: Date
+            if index < basalEntries.count - 1,
+               let nextEntryTime = dateFormatter.date(from: basalEntries[index + 1].start)
+            {
+                let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                entryEndTime = calendar.date(
+                    bySettingHour: nextEntryComponents.hour!,
+                    minute: nextEntryComponents.minute!,
+                    second: nextEntryComponents.second!,
+                    of: now
+                )!
+            } else {
+                entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
             }
 
-            tidepoolService.uploadPumpEventData(pumpEvents) { result in
-                switch result {
-                case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
-                case .success:
-                    debug(.nightscout, "Success synchronizing Pump Event data:")
-                }
+            if now >= entryStartTime, now < entryEndTime {
+                currentRate = entry
             }
         }
+
+        return currentRate
     }
+}
 
-    func uploadGlucose(device: HKDevice?) async {
-        // TODO: get correct glucose values
-        let glucose: [BloodGlucose] = await glucoseStorage.getGlucoseNotYetUploadedToNightscout()
+/// Glucose Upload Functionality
+extension BaseTidepoolManager {
+    func uploadGlucose() async {
+        uploadGlucose(await glucoseStorage.getGlucoseNotYetUploadedToTidepool())
+        uploadGlucose(
+            await glucoseStorage
+                .getManualGlucoseNotYetUploadedToTidepool()
+        )
+    }
 
+    func uploadGlucose(_ glucose: [StoredGlucoseSample]) {
         guard !glucose.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
-        let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id ?? UUID().uuidString) != nil }
-
-        let chunks = glucoseWithoutCorrectID.chunks(ofCount: tidepoolService.glucoseDataLimit ?? 100)
+        let chunks = glucose.chunks(ofCount: tidepoolService.glucoseDataLimit ?? 100)
 
         processQueue.async {
             for chunk in chunks {
-                // Link all glucose values with the current device
-                let chunkStoreGlucose = chunk.map { $0.convertStoredGlucoseSample(device: device) }
-
-                tidepoolService.uploadGlucoseData(chunkStoreGlucose) { result in
+                tidepoolService.uploadGlucoseData(chunk) { result in
                     switch result {
                     case .success:
-                        debug(.nightscout, "Success synchronizing glucose data:")
+                        debug(.nightscout, "Success synchronizing glucose data")
+
+                        // After successful upload, update the isUploadedToTidepool flag in Core Data
+                        Task {
+                            await self.updateGlucoseAsUploaded(glucose)
+                        }
                     case let .failure(error):
                         debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
-                        // self.uploadFailed(key)
                     }
                 }
             }
         }
     }
 
-    /// force to uploads all data in Tidepool Service
-    func forceUploadData(device: HKDevice?) {
-        Task {
-            uploadDose()
-            uploadCarbs()
-            await uploadGlucose(device: device)
-        }
-    }
-}
+    private func updateGlucoseAsUploaded(_ glucose: [StoredGlucoseSample]) async {
+        await backgroundContext.perform {
+            let ids = glucose.map(\.syncIdentifier) as NSArray
+            let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
-extension BaseTidepoolManager: PumpHistoryObserver {
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        uploadDose()
-    }
-}
-
-extension BaseTidepoolManager: CarbsObserver {
-    func carbsDidUpdate(_: [CarbsEntry]) {
-        uploadCarbs()
-    }
-}
-
-extension BaseTidepoolManager: TempTargetsObserver {
-    func tempTargetsDidUpdate(_: [TempTarget]) {}
-}
-
-extension BaseTidepoolManager: ServiceDelegate {
-    var hostIdentifier: String {
-        "com.loopkit.Loop" // To check
-    }
-
-    var hostVersion: String {
-        var semanticVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToTidepool = true
+                }
 
-        while semanticVersion.split(separator: ".").count < 3 {
-            semanticVersion += ".0"
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
+                )
+            }
         }
-
-        semanticVersion += "+\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String)"
-
-        return semanticVersion
     }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func enactRemoteOverride(name _: String, durationTime _: TimeInterval?, remoteAddress _: String) async throws {}
-
-    func cancelRemoteOverride() async throws {}
-
-    func deliverRemoteCarbs(
-        amountInGrams _: Double,
-        absorptionTime _: TimeInterval?,
-        foodType _: String?,
-        startDate _: Date?
-    ) async throws {}
-
-    func deliverRemoteBolus(amountInUnits _: Double) async throws {}
 }
 
 extension BaseTidepoolManager: StatefulPluggableDelegate {

+ 2 - 2
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -232,9 +232,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             fetchLimit: 3
         )
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
         }
     }

+ 31 - 26
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -148,10 +148,10 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             fetchLimit: 1
         )
 
-        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
         return await context.perform {
-            fetchedResults.map(\.objectID)
+            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
+            return fetchedResults.map(\.objectID)
         }
     }
 
@@ -166,10 +166,10 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             propertiesToFetch: ["enabled", "percentage", "objectID"]
         )
 
-        guard let fetchedResults = results as? [[String: Any]] else { return nil }
-
         return await context.perform {
-            fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }.first
+            guard let fetchedResults = results as? [[String: Any]] else { return nil }
+
+            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }.first
         }
     }
 
@@ -184,22 +184,22 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             batchSize: 12
         )
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return []
-        }
-
         return await context.perform {
-            glucoseResults.map(\.objectID)
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return []
+            }
+
+            return glucoseResults.map(\.objectID)
         }
     }
 
-    private func configureState() async {
-        let glucoseValuesIDs = await fetchGlucose()
-        async let lastDeterminationIDs = fetchlastDetermination()
-        async let latestOverrideID = fetchLatestOverride()
+    @MainActor private func configureState() async {
+        let glucoseValuesIds = await fetchGlucose()
+        async let lastDeterminationIds = fetchlastDetermination()
+        async let latestOverrideId = fetchLatestOverride()
 
-        guard let lastDeterminationID = await lastDeterminationIDs.first,
-              let latestOverrideID = await latestOverrideID
+        guard let lastDeterminationId = await lastDeterminationIds.first,
+              let latestOverrideId = await latestOverrideId
         else {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination/ last Override")
             return
@@ -207,14 +207,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
         do {
             let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: glucoseValuesIDs, context: viewContext)
+                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
 
-            let lastDetermination = try viewContext.existingObject(with: lastDeterminationID) as? OrefDetermination
-            let latestOverride = try viewContext.existingObject(with: latestOverrideID) as? OverrideStored
+            let lastDetermination = try viewContext.existingObject(with: lastDeterminationId) as? OrefDetermination
+            let latestOverride = try viewContext.existingObject(with: latestOverrideId) as? OverrideStored
 
             let recommendedInsulin = await newBolusCalc(
-                ids: glucoseValuesIDs,
-                determination: lastDetermination
+                glucoseIds: glucoseValuesIds,
+                determinationId: lastDeterminationId
             )
 
             await MainActor.run { [weak self] in
@@ -343,9 +343,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         return description
     }
 
-    private func newBolusCalc(ids: [NSManagedObjectID], determination: OrefDetermination?) async -> Decimal {
+    private func newBolusCalc(glucoseIds: [NSManagedObjectID], determinationId: NSManagedObjectID) async -> Decimal {
         await context.perform {
-            let glucoseObjects = ids.compactMap { self.context.object(with: $0) as? GlucoseStored }
+            let glucoseObjects = glucoseIds.compactMap { self.context.object(with: $0) as? GlucoseStored }
+            guard let determination = self.context.object(with: determinationId) as? OrefDetermination else {
+                print("Failed to fetch determination")
+                return 0
+            }
+
             guard let firstGlucose = glucoseObjects.first else {
                 return 0 // If there's no glucose data, exit the block
             }
@@ -359,8 +364,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
             let conversion: Decimal = self.settingsManager.settings.units == .mmolL ? 0.0555 : 1
             let isf = self.state.isf ?? 0
-            let target = determination?.currentTarget as? Decimal ?? 100
-            let carbratio = determination?.carbRatio as? Decimal ?? 10
+            let target = determination.currentTarget as? Decimal ?? 100
+            let carbratio = determination.carbRatio as? Decimal ?? 10
             let cob = self.state.cob ?? 0
             let iob = self.state.iob ?? 0
             let fattyMealFactor = self.settingsManager.settings.fattyMealFactor

+ 204 - 34
LiveActivity/LiveActivity.swift

@@ -17,6 +17,11 @@ enum GlucoseUnits: String, Equatable {
     static let exchangeRate: Decimal = 0.0555
 }
 
+enum GlucoseColorScheme: String, Equatable {
+    case staticColor
+    case dynamicColor
+}
+
 func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
     var result = Decimal()
     var toRound = value
@@ -60,6 +65,76 @@ extension NumberFormatter {
 }
 
 struct LiveActivity: Widget {
+    // Helper function to decide how to pick the glucose color
+    func getDynamicGlucoseColor(
+        glucoseValue: Decimal,
+        highGlucoseColorValue: Decimal,
+        lowGlucoseColorValue: Decimal,
+        targetGlucose: Decimal,
+        glucoseColorScheme: String,
+        offset: Decimal
+    ) -> Color {
+        // Convert Decimal to Int for high and low glucose values
+        let lowGlucose = lowGlucoseColorValue - offset
+        let highGlucose = highGlucoseColorValue + (offset * 1.75)
+        let targetGlucose = targetGlucose
+
+        // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
+        if glucoseColorScheme == "dynamicColor" {
+            return calculateHueBasedGlucoseColor(
+                glucoseValue: glucoseValue,
+                highGlucose: highGlucose,
+                lowGlucose: lowGlucose,
+                targetGlucose: targetGlucose
+            )
+        }
+        // Otheriwse, use static (orange = high, red = low, green = range)
+        else {
+            if glucoseValue > highGlucose {
+                return Color.orange
+            } else if glucoseValue < lowGlucose {
+                return Color.red
+            } else {
+                return Color.green
+            }
+        }
+    }
+
+    // Dynamic color - Define the hue values for the key points
+    // We'll shift color gradually one glucose point at a time
+    // We'll shift through the rainbow colors of ROY-G-BIV from low to high
+    // Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
+    func calculateHueBasedGlucoseColor(
+        glucoseValue: Decimal,
+        highGlucose: Decimal,
+        lowGlucose: Decimal,
+        targetGlucose: Decimal
+    ) -> Color {
+        let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
+        let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
+        let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
+
+        // Calculate the hue based on the bgLevel
+        var hue: CGFloat
+        if glucoseValue <= lowGlucose {
+            hue = redHue
+        } else if glucoseValue >= highGlucose {
+            hue = purpleHue
+        } else if glucoseValue <= targetGlucose {
+            // Interpolate between red and green
+            let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
+
+            hue = redHue + ratio * (greenHue - redHue)
+        } else {
+            // Interpolate between green and purple
+            let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
+            hue = greenHue + ratio * (purpleHue - greenHue)
+        }
+        // Return the color with full saturation and brightness
+        let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
+        return color
+    }
+
     private let dateFormatter: DateFormatter = {
         var f = DateFormatter()
         f.dateStyle = .none
@@ -179,7 +254,12 @@ struct LiveActivity: Widget {
         }
     }
 
-    private func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>, size: Size) -> (some View, Int) {
+    private func bgAndTrend(
+        context: ActivityViewContext<LiveActivityAttributes>,
+        size: Size,
+        hasStaticColorScheme: Bool,
+        glucoseColor: Color
+    ) -> (some View, Int) {
         var characters = 0
 
         let bgText = context.state.bg
@@ -188,16 +268,11 @@ struct LiveActivity: Widget {
         // narrow mode is for the minimal dynamic island view
         // there is not enough space to show all three arrow there
         // and everything has to be squeezed together to some degree
-        // only display the first arrow character and make it red in case there were more characters
+        // only display the first arrow character
         var directionText: String?
-        var warnColor: Color?
         if let direction = context.state.direction {
             if size == .compact {
                 directionText = String(direction[direction.startIndex ... direction.startIndex])
-
-                if direction.count > 1 {
-                    warnColor = Color.red
-                }
             } else {
                 directionText = direction
             }
@@ -214,17 +289,15 @@ struct LiveActivity: Widget {
 
         let stack = HStack(spacing: spacing) {
             Text(bgText)
+                .foregroundColor(hasStaticColorScheme ? .primary : glucoseColor)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
             if let direction = directionText {
                 let text = Text(direction)
                 switch size {
                 case .minimal:
                     let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
-                    if let warnColor {
-                        scaledText.foregroundStyle(warnColor)
-                    } else {
-                        scaledText
-                    }
+                    scaledText.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
                 case .compact:
                     text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
 
@@ -233,7 +306,7 @@ struct LiveActivity: Widget {
                 }
             }
         }
-        .foregroundStyle(context.isStale ? Color.primary.opacity(0.5) : Color.primary)
+        .foregroundColor(context.isStale ? Color.primary.opacity(0.5) : (hasStaticColorScheme ? .primary : glucoseColor))
 
         return (stack, characters)
     }
@@ -268,33 +341,61 @@ struct LiveActivity: Widget {
             let min = min(additionalState.chart.min() ?? 45, 40) - 20
             let max = max(additionalState.chart.max() ?? 270, 300) + 50
 
-            let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? additionalState.lowGlucose : additionalState.lowGlucose
+            let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? context.state.lowGlucose : context.state.lowGlucose
                 .asMmolL
-            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? additionalState.highGlucose : additionalState.highGlucose
+            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? context.state.highGlucose : context.state.highGlucose
                 .asMmolL
 
+            // TODO: grab target from proper targets, do not hard code.
+            let highColor = getDynamicGlucoseColor(
+                glucoseValue: yAxisRuleMarkMax,
+                highGlucoseColorValue: yAxisRuleMarkMax,
+                lowGlucoseColorValue: yAxisRuleMarkMin,
+                targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
+                glucoseColorScheme: context.state.glucoseColorScheme,
+                offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
+            )
+
+            // TODO: grab target from proper targets, do not hard code.
+            let lowColor = getDynamicGlucoseColor(
+                glucoseValue: yAxisRuleMarkMin,
+                highGlucoseColorValue: yAxisRuleMarkMax,
+                lowGlucoseColorValue: yAxisRuleMarkMin,
+                targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
+                glucoseColorScheme: context.state.glucoseColorScheme,
+                offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
+            )
+
             Chart {
-                RuleMark(y: .value("Low", yAxisRuleMarkMin))
-                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
                 RuleMark(y: .value("High", yAxisRuleMarkMax))
+                    .foregroundStyle(highColor)
+                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
+                RuleMark(y: .value("Low", yAxisRuleMarkMin))
+                    .foregroundStyle(lowColor)
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
 
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                     let currentValue = additionalState.chart[index]
                     let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
+
+                    // TODO: grab target from proper targets, do not hard code.
+                    let pointMarkColor = self.getDynamicGlucoseColor(
+                        glucoseValue: currentValue,
+                        highGlucoseColorValue: context.state.highGlucose,
+                        lowGlucoseColorValue: context.state.lowGlucose,
+                        targetGlucose: 90,
+                        glucoseColorScheme: context.state.glucoseColorScheme,
+                        offset: 20
+                    )
+
                     let chartDate = additionalState.chartDate[index] ?? Date()
+
                     let pointMark = PointMark(
                         x: .value("Time", chartDate),
                         y: .value("Value", displayValue)
                     ).symbolSize(15)
 
-                    if displayValue > yAxisRuleMarkMax {
-                        pointMark.foregroundStyle(Color.orange.gradient)
-                    } else if displayValue < yAxisRuleMarkMin {
-                        pointMark.foregroundStyle(Color.red.gradient)
-                    } else {
-                        pointMark.foregroundStyle(Color.green.gradient)
-                    }
+                    pointMark.foregroundStyle(pointMarkColor)
                 }
             }
             .chartYAxis {
@@ -313,9 +414,23 @@ struct LiveActivity: Widget {
     }
 
     @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+        // TODO: grab target from proper targets, do not hard code.
+        let glucoseColor = getDynamicGlucoseColor(
+            glucoseValue: Decimal(string: context.state.bg) ?? 100,
+            highGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? context.state.highGlucose : context.state
+                .highGlucose.asMmolL,
+            lowGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? context.state.lowGlucose : context.state
+                .lowGlucose.asMmolL,
+            targetGlucose: context.state.detailedViewState?.unit == "mg/dL" ? 90 : 90.asMmolL,
+            glucoseColorScheme: context.state.glucoseColorScheme,
+            offset: context.state.detailedViewState?.unit == "mg/dL" ? 20 : 20.asMmolL
+        )
+
         if let detailedViewState = context.state.detailedViewState {
             HStack(spacing: 12) {
-                chart(context: context, additionalState: detailedViewState).frame(maxWidth: UIScreen.main.bounds.width / 1.8)
+                chart(context: context, additionalState: detailedViewState)
+                    .frame(maxWidth: UIScreen.main.bounds.width / 1.8)
                 VStack(alignment: .leading) {
                     Spacer()
                     bgLabel(context: context, additionalState: detailedViewState)
@@ -347,11 +462,21 @@ struct LiveActivity: Widget {
                     }
                 } else {
                     HStack(spacing: 3) {
-                        bgAndTrend(context: context, size: .expanded).0.font(.title)
+                        bgAndTrend(
+                            context: context,
+                            size: .expanded,
+                            hasStaticColorScheme: hasStaticColorScheme,
+                            glucoseColor: glucoseColor
+                        ).0.font(.title)
                         Spacer()
                         VStack(alignment: .trailing, spacing: 5) {
                             changeLabel(context: context).font(.title3)
-                            updatedLabel(context: context).font(.caption).foregroundStyle(.primary.opacity(0.7))
+                                .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
+                            updatedLabel(context: context).font(.caption)
+                                .foregroundStyle(
+                                    hasStaticColorScheme ? .primary
+                                        .opacity(0.7) : glucoseColor
+                                )
                         }
                     }
                 }
@@ -368,12 +493,33 @@ struct LiveActivity: Widget {
     }
 
     func dynamicIsland(context: ActivityViewContext<LiveActivityAttributes>) -> DynamicIsland {
-        DynamicIsland {
+        let glucoseValueForColor = context.state.bg
+        let highGlucose = context.state.highGlucose
+        let lowGlucose = context.state.lowGlucose
+
+        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+        // TODO: grab target from proper targets, do not hard code.
+        let glucoseColor = getDynamicGlucoseColor(
+            glucoseValue: Decimal(string: glucoseValueForColor) ?? 100,
+            highGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? highGlucose : highGlucose.asMmolL,
+            lowGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? lowGlucose : lowGlucose.asMmolL,
+            targetGlucose: context.state.detailedViewState?.unit == "mg/dL" ? 90 : 90.asMmolL,
+            glucoseColorScheme: context.state.glucoseColorScheme,
+            offset: context.state.detailedViewState?.unit == "mg/dL" ? 20 : 20.asMmolL
+        )
+
+        return DynamicIsland {
             DynamicIslandExpandedRegion(.leading) {
-                bgAndTrend(context: context, size: .expanded).0.font(.title2).padding(.leading, 5)
+                bgAndTrend(
+                    context: context,
+                    size: .expanded,
+                    hasStaticColorScheme: hasStaticColorScheme,
+                    glucoseColor: glucoseColor
+                ).0.font(.title2).padding(.leading, 5)
             }
             DynamicIslandExpandedRegion(.trailing) {
                 changeLabel(context: context).font(.title2).padding(.trailing, 5)
+                    .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
             }
             DynamicIslandExpandedRegion(.bottom) {
                 if context.state.isInitialState {
@@ -396,11 +542,17 @@ struct LiveActivity: Widget {
                 }
             }
         } compactLeading: {
-            bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
+            bgAndTrend(context: context, size: .compact, hasStaticColorScheme: hasStaticColorScheme, glucoseColor: glucoseColor).0
+                .padding(.leading, 4)
         } compactTrailing: {
-            changeLabel(context: context).padding(.trailing, 4)
+            changeLabel(context: context).padding(.trailing, 4).foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
         } minimal: {
-            let (_label, characterCount) = bgAndTrend(context: context, size: .minimal)
+            let (_label, characterCount) = bgAndTrend(
+                context: context,
+                size: .minimal,
+                hasStaticColorScheme: hasStaticColorScheme,
+                glucoseColor: glucoseColor
+            )
             let label = _label.padding(.leading, 7).padding(.trailing, 3)
 
             if characterCount < 4 {
@@ -412,7 +564,7 @@ struct LiveActivity: Widget {
             }
         }
         .widgetURL(URL(string: "Trio://"))
-        .keylineTint(Color.purple)
+        .keylineTint(hasStaticColorScheme ? Color.purple : glucoseColor)
         .contentMargins(.horizontal, 0, for: .minimal)
         .contentMargins(.trailing, 0, for: .compactLeading)
         .contentMargins(.leading, 0, for: .compactTrailing)
@@ -435,10 +587,13 @@ private extension LiveActivityAttributes.ContentState {
     // Use mmol/l notation with decimal point as well for the same reason, it uses up to 4 characters, while mg/dl uses up to 3
     static var testWide: LiveActivityAttributes.ContentState {
         LiveActivityAttributes.ContentState(
-            bg: "00.0",
+            bg: 00.0.description,
             direction: "→",
             change: "+0.0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -450,6 +605,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑↑",
             change: "+0.0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -461,6 +619,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑↑↑",
             change: "+0.0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -473,6 +634,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑",
             change: "+0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -484,6 +648,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↗︎",
             change: "+00",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -495,6 +662,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: nil,
             change: "--",
             date: Date().addingTimeInterval(-60 * 60),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: true
         )

+ 2 - 0
Model/Classes+Properties/CarbEntryStored+CoreDataProperties.swift

@@ -13,6 +13,8 @@ public extension CarbEntryStored {
     @NSManaged var id: UUID?
     @NSManaged var isFPU: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var protein: Double
 }

+ 2 - 0
Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift

@@ -12,6 +12,8 @@ public extension GlucoseStored {
     @NSManaged var id: UUID?
     @NSManaged var isManual: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
 }
 
 extension GlucoseStored: Identifiable {}

+ 2 - 0
Model/Classes+Properties/PumpEventStored+CoreDataProperties.swift

@@ -8,6 +8,8 @@ public extension PumpEventStored {
 
     @NSManaged var id: String?
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var timestamp: Date?
     @NSManaged var type: String?

+ 19 - 1
Model/Helper/CarbEntryStored+helper.swift

@@ -22,6 +22,24 @@ extension NSPredicate {
         )
     }
 
+    static var carbsNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToHealth == %@",
+            date as NSDate,
+            false as NSNumber
+        )
+    }
+
+    static var carbsNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToTidepool == %@",
+            date as NSDate,
+            false as NSNumber
+        )
+    }
+
     static var fpusNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
@@ -71,7 +89,7 @@ extension CarbEntryStored: Encodable {
         try container.encode(formattedDate, forKey: .created_at)
 
         // TODO: handle this conditionally; pass in the enteredBy string (manual entry or via NS or Apple Health)
-        try container.encode("Open-iAPS", forKey: .enteredBy)
+        try container.encode("Trio", forKey: .enteredBy)
 
         try container.encode(carbs, forKey: .carbs)
         try container.encode(fat, forKey: .fat)

+ 30 - 0
Model/Helper/GlucoseStored+helper.swift

@@ -70,6 +70,16 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
     }
 
+    static var glucoseNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
+    }
+
+    static var glucoseNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
+    }
+
     static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
@@ -79,6 +89,26 @@ extension NSPredicate {
             true as NSNumber
         )
     }
+
+    static var manualGlucoseNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToHealth == %@ AND isManual == %@",
+            date as NSDate,
+            false as NSNumber,
+            true as NSNumber
+        )
+    }
+
+    static var manualGlucoseNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToTidepool == %@ AND isManual == %@",
+            date as NSDate,
+            false as NSNumber,
+            true as NSNumber
+        )
+    }
 }
 
 extension GlucoseStored: Encodable {

+ 10 - 0
Model/Helper/PumpEvent+helper.swift

@@ -82,6 +82,16 @@ extension NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
     }
+
+    static var pumpEventsNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "timestamp >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
+    }
+
+    static var pumpEventsNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "timestamp >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
+    }
 }
 
 // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history

+ 10 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -13,7 +13,9 @@
         <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="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" 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">
@@ -44,9 +46,11 @@
         <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="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
-            <fetchIndexElement property="date" type="Binary" order="descending"/>
+            <fetchIndexElement property="date" type="Binary" order="ascending"/>
         </fetchIndex>
         <fetchIndex name="byIsManual">
             <fetchIndexElement property="isManual" type="Binary" order="ascending"/>
@@ -54,6 +58,9 @@
         <fetchIndex name="byIsUploadedToNS">
             <fetchIndexElement property="isUploadedToNS" type="Binary" order="ascending"/>
         </fetchIndex>
+        <fetchIndex name="byIsUploadedToHealth">
+            <fetchIndexElement property="isUploadedToHealth" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     <entity name="LoopStatRecord" representedClassName="LoopStatRecord" syncable="YES">
         <attribute name="duration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
@@ -161,7 +168,9 @@
     </entity>
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
         <attribute name="id" optional="YES" attributeType="String"/>
+        <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" 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"/>

+ 1 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -49,7 +49,7 @@
     {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"