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

Merge branch 'core-data-sync-trio' into override-update

Mike Plante 1 год назад
Родитель
Сommit
03dfeeec2e
81 измененных файлов с 3676 добавлено и 2172 удалено
  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. 146 14
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  8. 38 4
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  9. 176 10
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  10. 10 10
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  11. 124 3
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  12. 15 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  13. 67 0
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  14. 3 2
      FreeAPS/Sources/Models/BloodGlucose.swift
  15. 1 1
      FreeAPS/Sources/Models/CarbsEntry.swift
  16. 15 0
      FreeAPS/Sources/Models/ColorSchemeOption.swift
  17. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  18. 22 0
      FreeAPS/Sources/Models/GlucoseColorScheme.swift
  19. 47 34
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  20. 109 98
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  21. 61 56
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  22. 45 58
      FreeAPS/Sources/Modules/Bolus/View/PopupView.swift
  23. 7 2
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  24. 36 34
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  25. 122 64
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  26. 1 1
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  27. 18 18
      FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  28. 16 14
      FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift
  29. 6 0
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  30. 32 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  31. 60 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  32. 103 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  33. 58 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  34. 106 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  35. 38 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  36. 98 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  37. 81 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  38. 116 528
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  39. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/CobChart.swift
  40. 26 4
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  41. 15 3
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  42. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/IobChart.swift
  43. 26 101
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  44. 50 36
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  45. 4 0
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  46. 7 2
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  47. 14 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  48. 47 47
      FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  49. 3 1
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  50. 1 1
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  51. 29 5
      FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift
  52. 2 2
      FreeAPS/Sources/Modules/Stat/StatStateModel.swift
  53. 2 0
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  54. 1 0
      FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  55. 3 0
      FreeAPS/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  56. 76 0
      FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  57. 26 24
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  58. 500 487
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  59. 12 12
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  60. 3 3
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  61. 3 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  62. 24 7
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  63. 37 15
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  64. 474 245
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  65. 25 20
      FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift
  66. 60 49
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  67. 1 1
      LibreTransmitter
  68. 204 34
      LiveActivity/LiveActivity.swift
  69. 2 0
      Model/Classes+Properties/CarbEntryStored+CoreDataProperties.swift
  70. 2 0
      Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift
  71. 2 0
      Model/Classes+Properties/PumpEventStored+CoreDataProperties.swift
  72. 15 36
      Model/CoreDataObserver.swift
  73. 19 1
      Model/Helper/CarbEntryStored+helper.swift
  74. 0 4
      Model/Helper/CustomNotification.swift
  75. 30 0
      Model/Helper/GlucoseStored+helper.swift
  76. 10 0
      Model/Helper/PumpEvent+helper.swift
  77. 10 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  78. 1 1
      OmniBLE
  79. 1 1
      OmniKit
  80. 1 1
      TidepoolService
  81. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 64 - 12
FreeAPS.xcodeproj/project.pbxproj

@@ -256,6 +256,14 @@
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
 		5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+helper.swift */; };
 		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 */; };
 		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 */; };
 		5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864E8582C42CFAE00294306 /* DeterminationStorage.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
@@ -318,7 +326,7 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.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 */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
@@ -439,7 +447,10 @@
 		DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */; };
 		DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */; };
 		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
 		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		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 */; };
 		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 */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
 		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
@@ -907,6 +918,14 @@
 		583684072BD195A700070A60 /* Determination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Determination.swift; sourceTree = "<group>"; };
 		583684072BD195A700070A60 /* Determination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Determination.swift; sourceTree = "<group>"; };
 		5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+helper.swift"; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		5887527B2BD986E1008B081D /* OpenAPSBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSBattery.swift; sourceTree = "<group>"; };
@@ -970,7 +989,7 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.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>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.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>"; };
 		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>"; };
 		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>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
@@ -1093,7 +1112,10 @@
 		DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalInsulinDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalInsulinDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportResultView.swift; sourceTree = "<group>"; };
 		DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportResultView.swift; sourceTree = "<group>"; };
 		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
@@ -1552,6 +1574,7 @@
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
+				58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */,
 				3811DE2C25C9D49500A708ED /* View */,
 				3811DE2C25C9D49500A708ED /* View */,
 			);
 			);
 			path = Home;
 			path = Home;
@@ -1893,6 +1916,8 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
+				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				38A00B1E25FC00F7006BC0B0 /* Autotune.swift */,
 				38A00B1E25FC00F7006BC0B0 /* Autotune.swift */,
@@ -1943,6 +1968,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
@@ -2244,6 +2270,21 @@
 			path = Helper;
 			path = Helper;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
+			isa = PBXGroup;
+			children = (
+				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 */ = {
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -2346,7 +2387,7 @@
 			children = (
 			children = (
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
-				BDB899872C564509006F3298 /* ForeCastChart.swift */,
+				BDB899872C564509006F3298 /* ForecastChart.swift */,
 				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
 				BD0B2EF22C5998E600B3298F /* MealPresetView.swift */,
 				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
 				DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */,
 			);
 			);
@@ -3152,6 +3193,7 @@
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
+				58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
@@ -3177,6 +3219,7 @@
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
+				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
@@ -3230,6 +3273,7 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
+				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
@@ -3274,10 +3318,12 @@
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
+				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
+				58645B9F2CA2D2BE008AFCE7 /* PumpHistorySetup.swift in Sources */,
 				DD17454E2C55CA4D00211FAC /* UnitsLimitsSettingsDataFlow.swift in Sources */,
 				DD17454E2C55CA4D00211FAC /* UnitsLimitsSettingsDataFlow.swift in Sources */,
 				DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */,
 				DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
@@ -3288,6 +3334,7 @@
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
+				DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
@@ -3332,6 +3379,7 @@
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
+				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
@@ -3372,13 +3420,15 @@
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
-				BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */,
+				BDB899882C564509006F3298 /* ForecastChart.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
+				58645BA12CA2D2F8008AFCE7 /* OverrideSetup.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,
 				CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */,
 				CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */,
 				BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */,
 				BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
+				58645B9D2CA2D275008AFCE7 /* DeterminationSetup.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
@@ -3395,6 +3445,7 @@
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
 				CE7CA3522A064973004BE681 /* ListTempPresetsIntent.swift in Sources */,
 				CE7CA3522A064973004BE681 /* ListTempPresetsIntent.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
+				58645B9B2CA2D24F008AFCE7 /* CarbSetup.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
 				19A910362A24D6D700C8951B /* DateFilter.swift in Sources */,
 				19A910362A24D6D700C8951B /* DateFilter.swift in Sources */,
 				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
 				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
@@ -3505,6 +3556,7 @@
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
+				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
@@ -3773,7 +3825,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
@@ -3833,7 +3885,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
@@ -3862,7 +3914,7 @@
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(inherited)",
 					"@executable_path/Frameworks",
 					"@executable_path/Frameworks",
@@ -3903,7 +3955,7 @@
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(inherited)",
 					"@executable_path/Frameworks",
 					"@executable_path/Frameworks",
@@ -4088,7 +4140,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(inherited)",
 					"@executable_path/Frameworks",
 					"@executable_path/Frameworks",
@@ -4109,7 +4161,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(inherited)",
 					"@executable_path/Frameworks",
 					"@executable_path/Frameworks",
@@ -4139,7 +4191,7 @@
 				INFOPLIST_FILE = LiveActivity/Info.plist;
 				INFOPLIST_FILE = LiveActivity/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
 				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(inherited)",
 					"@executable_path/Frameworks",
 					"@executable_path/Frameworks",
@@ -4173,7 +4225,7 @@
 				INFOPLIST_FILE = LiveActivity/Info.plist;
 				INFOPLIST_FILE = LiveActivity/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
 				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
-				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(inherited)",
 					"@executable_path/Frameworks",
 					"@executable_path/Frameworks",

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

@@ -348,6 +348,8 @@
       buildConfiguration = "Debug"
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      enableThreadSanitizer = "YES"
+      enableUBSanitizer = "YES"
       launchStyle = "0"
       launchStyle = "0"
       useCustomWorkingDirectory = "NO"
       useCustomWorkingDirectory = "NO"
       ignoresPersistentStateOnLaunch = "NO"
       ignoresPersistentStateOnLaunch = "NO"
@@ -364,6 +366,20 @@
             ReferencedContainer = "container:FreeAPS.xcodeproj">
             ReferencedContainer = "container:FreeAPS.xcodeproj">
          </BuildableReference>
          </BuildableReference>
       </BuildableProductRunnable>
       </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>
       <EnvironmentVariables>
          <EnvironmentVariable
          <EnvironmentVariable
             key = "CG_NUMERICS_SHOW_BACKTRACE"
             key = "CG_NUMERICS_SHOW_BACKTRACE"

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

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

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

@@ -939,11 +939,13 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
             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...
     // TODO: - Refactor this whole shit here...

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

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

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

@@ -102,17 +102,19 @@ final class OpenAPS {
             fetchLimit: 2
             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
             batchSize: 24
         )
         )
 
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return ""
-        }
-
         return await context.perform {
         return await context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return ""
+            }
+
             // convert to JSON
             // convert to JSON
             return self.jsonConverter.convertToJSON(glucoseResults)
             return self.jsonConverter.convertToJSON(glucoseResults)
         }
         }
@@ -159,11 +161,11 @@ final class OpenAPS {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbResults = results as? [CarbEntryStored] else {
-            return ""
-        }
-
         let json = await context.perform {
         let json = await context.perform {
+            guard let carbResults = results as? [CarbEntryStored] else {
+                return ""
+            }
+
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
 
 
             if let additionalCarbs = additionalCarbs {
             if let additionalCarbs = additionalCarbs {
@@ -209,11 +211,11 @@ final class OpenAPS {
             batchSize: 50
             batchSize: 50
         )
         )
 
 
-        guard let pumpEventResults = results as? [PumpEventStored] else {
-            return nil
-        }
-
         return await context.perform {
         return await context.perform {
+            guard let pumpEventResults = results as? [PumpEventStored] else {
+                return nil
+            }
+
             return pumpEventResults.map(\.objectID)
             return pumpEventResults.map(\.objectID)
         }
         }
     }
     }

+ 146 - 14
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import SwiftDate
 import SwiftDate
@@ -8,12 +9,16 @@ protocol CarbsObserver {
 }
 }
 
 
 protocol CarbsStorage {
 protocol CarbsStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func recent() -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
+    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
 }
 }
 
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -24,6 +29,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
@@ -35,8 +46,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
         }
 
 
-        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+
+        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
     }
 
 
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
@@ -54,6 +66,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
 
 
         // Extract dates into a set for efficient lookup
         // 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 })
         let existingTimestamps = Set(existing24hCarbEntries.compactMap { $0["date"] as? Date })
 
 
         // Remove all entries that have a matching date in existingTimestamps
         // Remove all entries that have a matching date in existingTimestamps
@@ -106,7 +119,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      */
      */
     private func processFPU(
     private func processFPU(
-        entries _: [CarbsEntry],
+        entries: [CarbsEntry],
         fat: Decimal,
         fat: Decimal,
         protein: Decimal,
         protein: Decimal,
         createdAt: Date,
         createdAt: Date,
@@ -135,7 +148,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var numberOfEquivalents = carbEquivalents / carbEquivalentSize
         var numberOfEquivalents = carbEquivalents / carbEquivalentSize
 
 
         var useDate = actualDate ?? createdAt
         var useDate = actualDate ?? createdAt
-        let fpuID = UUID().uuidString
+        let fpuID = entries.first?.fpuID ?? UUID().uuidString
         var futureCarbArray = [CarbsEntry]()
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
         var firstIndex = true
 
 
@@ -152,7 +165,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 fat: 0,
                 fat: 0,
                 protein: 0,
                 protein: 0,
                 note: nil,
                 note: nil,
-                enteredBy: CarbsEntry.manual, isFPU: true,
+                enteredBy: CarbsEntry.manual,
+                isFPU: true,
                 fpuID: fpuID
                 fpuID: fpuID
             )
             )
             futureCarbArray.append(eachCarbEntry)
             futureCarbArray.append(eachCarbEntry)
@@ -193,6 +207,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.id = UUID()
             newItem.id = UUID()
             newItem.isFPU = false
             newItem.isFPU = false
             newItem.isUploadedToNS = areFetchedFromRemote ? true : 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 {
             do {
                 guard self.coredataContext.hasChanges else { return }
                 guard self.coredataContext.hasChanges else { return }
@@ -204,8 +224,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
     }
 
 
     private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
     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
         var entrySlice = ArraySlice(entries) // convert to ArraySlice
         let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
         let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
             guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
             guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
@@ -219,6 +241,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             carbEntry.fpuID = commonFPUID
             carbEntry.fpuID = commonFPUID
             carbEntry.isFPU = true
             carbEntry.isFPU = true
             carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
             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
             return false // return false to continue
         }
         }
         await coredataContext.perform {
         await coredataContext.perform {
@@ -226,8 +249,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 try self.coredataContext.execute(batchInsert)
                 try self.coredataContext.execute(batchInsert)
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
 
 
-                // Send notification for triggering a fetch in Home State Model to update the FPU Array
-                Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
+                // Notify subscriber in Home State Model to update the FPU Array
+                self.updateSubject.send(())
             } catch {
             } catch {
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
             }
             }
@@ -242,6 +265,53 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
     }
 
 
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteCarbs"
+
+        var carbEntry: CarbEntryStored?
+
+        await taskContext.perform {
+            do {
+                carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                guard let carbEntry = carbEntry else {
+                    debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
+                    return
+                }
+
+                if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                    // fetch request for all carb entries with the same id
+                    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
+                    fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
+
+                    // NSBatchDeleteRequest
+                    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
+                    deleteRequest.resultType = .resultTypeCount
+
+                    // execute the batch delete request
+                    let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
+                    debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
+
+                    // Notifiy subscribers of the batch delete
+                    self.updateSubject.send(())
+                } else {
+                    taskContext.delete(carbEntry)
+
+                    guard taskContext.hasChanges else { return }
+                    try taskContext.save()
+
+                    debugPrint(
+                        "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
+                    )
+                }
+
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+            }
+        }
+    }
+
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
         processQueue.sync {
         processQueue.sync {
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
@@ -281,11 +351,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
-
         return await coredataContext.perform {
         return await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                return []
+            }
+
             return carbEntries.map { result in
             return carbEntries.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
                     duration: nil,
                     duration: nil,
@@ -320,9 +390,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
-
         return await coredataContext.perform {
         return await coredataContext.perform {
+            guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
+
             return fpuEntries.map { result in
             return fpuEntries.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
                     duration: nil,
                     duration: nil,
@@ -346,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
+                )
+            }
+        }
+    }
 }
 }

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

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject
@@ -6,6 +7,10 @@ protocol DeterminationStorage {
     func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
     func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: 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?
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
 }
 }
 
 
@@ -27,10 +32,10 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
-            fetchedResults.map(\.objectID)
+            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
+            return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
@@ -38,7 +43,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         await context.perform {
         await context.perform {
             do {
             do {
                 guard let determination = try context.existingObject(with: determinationID) as? OrefDetermination,
                 guard let determination = try context.existingObject(with: determinationID) as? OrefDetermination,
-                      let forecastSet = determination.forecasts as? Set<NSManagedObject>
+                      let forecastSet = determination.forecasts
                 else {
                 else {
                     return []
                     return []
                 }
                 }
@@ -72,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
     // Convert NSDecimalNumber to Decimal
     func decimal(from nsDecimalNumber: NSDecimalNumber?) -> Decimal {
     func decimal(from nsDecimalNumber: NSDecimalNumber?) -> Decimal {
         nsDecimalNumber?.decimalValue ?? 0.0
         nsDecimalNumber?.decimalValue ?? 0.0

+ 176 - 10
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -1,11 +1,14 @@
 import AVFAudio
 import AVFAudio
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import LoopKit
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
 protocol GlucoseStorage {
 protocol GlucoseStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeGlucose(_ glucose: [BloodGlucose])
     func storeGlucose(_ glucose: [BloodGlucose])
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
@@ -15,7 +18,12 @@ protocol GlucoseStorage {
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getManualGlucoseNotYetUploadedToNightscout() 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 }
     var alarm: GlucoseAlarm? { get }
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
 }
 
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -26,6 +34,12 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     private enum Config {
     private enum Config {
         static let filterTime: TimeInterval = 3.5 * 60
         static let filterTime: TimeInterval = 3.5 * 60
     }
     }
@@ -80,6 +94,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
                         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
                         return false // Continue processing
                     }
                     }
                 )
                 )
@@ -87,12 +103,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 // process batch insert
                 // process batch insert
                 do {
                 do {
                     try self.coredataContext.execute(batchInsert)
                     try self.coredataContext.execute(batchInsert)
-//                    debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
 
 
-                    // Send notification for triggering a fetch in Home State Model to update the Glucose Array
-                    /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
-                    /// But I do not want to fetch on the Main Thread using the @FetchRequest property, I also can not use the FetchedResultsController because of the architecture of the State Model (it must inherit from BaseStateModel and therefore can not inherit from NSObject as well) and because of the fact that I am using a batch insert here there are no notifications sent from the managedObjectContext because changes are directly stored in the persistent container
-                    Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
+                    // Notify subscribers that there is a new glucose value
+                    // We need to do this because the due to the batch insert there is no ManagedObjectContext notification
+                    self.updateSubject.send(())
                 } catch {
                 } catch {
                     debugPrint(
                     debugPrint(
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
@@ -235,7 +249,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
     }
 
 
     // Fetch glucose that is not uploaded to Nightscout yet
     // 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] {
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
@@ -246,9 +260,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
         return await coredataContext.perform {
         return await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -266,7 +280,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
     }
 
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     // 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] {
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
@@ -289,7 +303,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     rate: nil,
                     rate: nil,
                     eventType: .capillaryGlucose,
                     eventType: .capillaryGlucose,
                     createdAt: result.date,
                     createdAt: result.date,
-                    enteredBy: "Trio",
+                    enteredBy: CarbsEntry.manual,
                     bolus: nil,
                     bolus: nil,
                     insulin: nil,
                     insulin: nil,
                     notes: "Trio User",
                     notes: "Trio User",
@@ -320,6 +334,158 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
         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? {
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         coredataContext.performAndWait {
         coredataContext.performAndWait {

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

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

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

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -9,10 +10,13 @@ protocol PumpHistoryObserver {
 }
 }
 
 
 protocol PumpHistoryStorage {
 protocol PumpHistoryStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storePumpEvents(_ events: [NewPumpEvent])
     func storePumpEvents(_ events: [NewPumpEvent])
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func recent() -> [PumpHistoryEvent]
     func recent() -> [PumpHistoryEvent]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
     func deleteInsulin(at date: Date)
     func deleteInsulin(at date: Date)
 }
 }
 
 
@@ -22,6 +26,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settings: SettingsManager!
     @Injected() private var settings: SettingsManager!
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
@@ -72,6 +82,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                                         existingEvent.bolus?.amount = amount as NSDecimalNumber
                                         existingEvent.bolus?.amount = amount as NSDecimalNumber
                                         existingEvent.bolus?.isSMB = dose.automatic ?? true
                                         existingEvent.bolus?.isSMB = dose.automatic ?? true
                                         existingEvent.isUploadedToNS = false
                                         existingEvent.isUploadedToNS = false
+                                        existingEvent.isUploadedToHealth = false
+                                        existingEvent.isUploadedToTidepool = false
 
 
                                         print("Updated existing event with smaller value: \(amount)")
                                         print("Updated existing event with smaller value: \(amount)")
                                     }
                                     }
@@ -85,6 +97,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                         let newBolusEntry = BolusStored(context: self.context)
                         let newBolusEntry = BolusStored(context: self.context)
                         newBolusEntry.pumpEvent = newPumpEvent
                         newBolusEntry.pumpEvent = newPumpEvent
@@ -114,6 +128,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = date
                         newPumpEvent.timestamp = date
                         newPumpEvent.type = PumpEvent.tempBasal.rawValue
                         newPumpEvent.type = PumpEvent.tempBasal.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                         let newTempBasal = TempBasalStored(context: self.context)
                         let newTempBasal = TempBasalStored(context: self.context)
                         newTempBasal.pumpEvent = newPumpEvent
                         newTempBasal.pumpEvent = newPumpEvent
@@ -132,6 +148,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
                         newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .resume:
                     case .resume:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -144,6 +162,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpResume.rawValue
                         newPumpEvent.type = PumpEvent.pumpResume.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .rewind:
                     case .rewind:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -156,6 +176,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.rewind.rawValue
                         newPumpEvent.type = PumpEvent.rewind.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .prime:
                     case .prime:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -168,6 +190,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.prime.rawValue
                         newPumpEvent.type = PumpEvent.prime.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .alarm:
                     case .alarm:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -180,6 +204,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
                         newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
                         newPumpEvent.note = event.title
                         newPumpEvent.note = event.title
 
 
                     default:
                     default:
@@ -190,6 +216,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                 do {
                 do {
                     guard self.context.hasChanges else { return }
                     guard self.context.hasChanges else { return }
                     try self.context.save()
                     try self.context.save()
+
+                    self.updateSubject.send(())
                     debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
                     debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
                 } catch let error as NSError {
                 } catch let error as NSError {
                     debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
                     debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
@@ -207,6 +235,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             newPumpEvent.timestamp = timestamp
             newPumpEvent.timestamp = timestamp
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.isUploadedToNS = false
             newPumpEvent.isUploadedToNS = false
+            newPumpEvent.isUploadedToHealth = false
+            newPumpEvent.isUploadedToTidepool = false
 
 
             // create bolus entry and specify relationship to pump event
             // create bolus entry and specify relationship to pump event
             let newBolusEntry = BolusStored(context: self.context)
             let newBolusEntry = BolusStored(context: self.context)
@@ -218,6 +248,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             do {
             do {
                 guard self.context.hasChanges else { return }
                 guard self.context.hasChanges else { return }
                 try self.context.save()
                 try self.context.save()
+
+                self.updateSubject.send(())
             } catch {
             } catch {
                 print(error.localizedDescription)
                 print(error.localizedDescription)
             }
             }
@@ -262,10 +294,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
-
         return await context.perform { [self] in
         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 {
                 switch event.type {
                 case PumpEvent.bolus.rawValue:
                 case PumpEvent.bolus.rawValue:
                     // eventType determines whether bolus is external, smb or manual (=administered via app by user)
                     // eventType determines whether bolus is external, smb or manual (=administered via app by user)
@@ -411,4 +443,93 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             }.compactMap { $0 }
             }.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 }
+        }
+    }
 }
 }

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

@@ -10,6 +10,9 @@ import Swinject
 
 
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
 
+    // Read the color scheme preference from UserDefaults; defaults to system default setting
+    @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
     let coreDataStack = CoreDataStack.shared
     let coreDataStack = CoreDataStack.shared
 
 
     // Dependencies Assembler
     // Dependencies Assembler
@@ -67,6 +70,7 @@ import Swinject
     var body: some Scene {
     var body: some Scene {
         WindowGroup {
         WindowGroup {
             Main.RootView(resolver: resolver)
             Main.RootView(resolver: resolver)
+                .preferredColorScheme(colorScheme(for: colorSchemePreference ?? .systemDefault) ?? nil)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environmentObject(Icons())
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)
                 .onOpenURL(perform: handleURL)
@@ -85,6 +89,17 @@ import Swinject
         }
         }
     }
     }
 
 
+    private func colorScheme(for colorScheme: ColorSchemeOption) -> ColorScheme? {
+        switch colorScheme {
+        case .systemDefault:
+            return nil // Uses the system theme.
+        case .light:
+            return .light
+        case .dark:
+            return .dark
+        }
+    }
+
     func scheduleDatabaseCleaning() {
     func scheduleDatabaseCleaning() {
         let request = BGAppRefreshTaskRequest(identifier: "com.openiaps.cleanup")
         let request = BGAppRefreshTaskRequest(identifier: "com.openiaps.cleanup")
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days

+ 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 {
 extension BloodGlucose {
-    func convertStoredGlucoseSample(device: HKDevice?) -> StoredGlucoseSample {
+    func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
         StoredGlucoseSample(
         StoredGlucoseSample(
             syncIdentifier: id,
             syncIdentifier: id,
             startDate: dateString.date,
             startDate: dateString.date,
             quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
             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),
             grams: Double(carbs),
             startDate: createdAt,
             startDate: createdAt,
             uuid: UUID(uuidString: id!),
             uuid: UUID(uuidString: id!),
-            provenanceIdentifier: enteredBy ?? "",
+            provenanceIdentifier: enteredBy ?? "Trio",
             syncIdentifier: id,
             syncIdentifier: id,
             syncVersion: nil,
             syncVersion: nil,
             userCreatedDate: nil,
             userCreatedDate: nil,

+ 15 - 0
FreeAPS/Sources/Models/ColorSchemeOption.swift

@@ -0,0 +1,15 @@
+enum ColorSchemeOption: String, JSON, CaseIterable, Identifiable {
+    var id: String { rawValue }
+
+    case systemDefault
+    case light
+    case dark
+
+    var displayName: String {
+        switch self {
+        case .systemDefault: return "System Default"
+        case .light: return "Light"
+        case .dark: return "Dark"
+        }
+    }
+}

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

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

+ 47 - 34
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -19,6 +20,7 @@ extension Bolus {
 
 
         @Published var lowGlucose: Decimal = 70
         @Published var lowGlucose: Decimal = 70
         @Published var highGlucose: Decimal = 180
         @Published var highGlucose: Decimal = 180
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
 
 
         @Published var predictions: Predictions?
         @Published var predictions: Predictions?
         @Published var amount: Decimal = 0
         @Published var amount: Decimal = 0
@@ -115,22 +117,29 @@ extension Bolus {
         let now = Date.now
         let now = Date.now
 
 
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-        let backgroundContext = CoreDataStack.shared.newTaskContext()
+        let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
+        let determinationFetchContext = CoreDataStack.shared.newTaskContext()
 
 
-        private var coreDataObserver: CoreDataObserver?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
+            coreDataPublisher =
+                changedObjectsOnManagedObjectContextDidSavePublisher()
+                    .receive(on: DispatchQueue.global(qos: .background))
+                    .share()
+                    .eraseToAnyPublisher()
+            registerHandlers()
+            registerSubscribers()
+            setupBolusStateConcurrently()
+        }
+
+        private func setupBolusStateConcurrently() {
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
-                        self.setupGlucoseNotification()
-                    }
-                    group.addTask {
-                        self.registerHandlers()
-                    }
-                    group.addTask {
                         self.setupGlucoseArray()
                         self.setupGlucoseArray()
                     }
                     }
                     group.addTask {
                     group.addTask {
@@ -226,6 +235,7 @@ extension Bolus {
             maxProtein = settings.settings.maxProtein
             maxProtein = settings.settings.maxProtein
             useFPUconversion = settingsManager.settings.useFPUconversion
             useFPUconversion = settingsManager.settings.useFPUconversion
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         }
         }
 
 
         private func getCurrentSettingValue(for type: SettingType) async {
         private func getCurrentSettingValue(for type: SettingType) async {
@@ -303,7 +313,7 @@ extension Bolus {
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
         func calculateInsulin() -> Decimal {
-            let isfForCalculation = units == .mmolL ? isf.asMgdL : isf
+            let isfForCalculation = isf
 
 
             // insulin needed for the current blood glucose
             // insulin needed for the current blood glucose
             targetDifference = currentBG - target
             targetDifference = currentBG - target
@@ -382,9 +392,16 @@ extension Bolus {
 
 
                 await saveMeal()
                 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()
                     return hideModal()
                 }
                 }
             }
             }
@@ -561,33 +578,29 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
 extension Bolus.StateModel {
 extension Bolus.StateModel {
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.setupDeterminationsArray()
                 await self.setupDeterminationsArray()
                 await self.updateForecasts()
                 await self.updateForecasts()
             }
             }
-        }
+        }.store(in: &subscriptions)
 
 
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.setupGlucoseArray()
             self.setupGlucoseArray()
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        setupGlucoseArray()
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupGlucoseArray()
+            }
+            .store(in: &subscriptions)
     }
     }
 }
 }
 
 
@@ -606,16 +619,16 @@ extension Bolus.StateModel {
     private func fetchGlucose() async -> [NSManagedObjectID] {
     private func fetchGlucose() async -> [NSManagedObjectID] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
             ascending: false,
             ascending: false,
             fetchLimit: 288
             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)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -651,16 +664,16 @@ extension Bolus.StateModel {
 
 
     private func mapForecastsForChart() async -> Determination? {
     private func mapForecastsForChart() async -> Determination? {
         let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
         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 {
             guard let determinationObject = determinationObjects.first else {
                 return nil
                 return nil
             }
             }
 
 
             let eventualBG = determinationObject.eventualBG?.intValue
             let eventualBG = determinationObject.eventualBG?.intValue
 
 
-            let forecastsSet = determinationObject.forecasts as? Set<Forecast> ?? []
+            let forecastsSet = determinationObject.forecasts ?? []
             let predictions = Predictions(
             let predictions = Predictions(
                 iob: forecastsSet.extractValues(for: "iob"),
                 iob: forecastsSet.extractValues(for: "iob"),
                 zt: forecastsSet.extractValues(for: "zt"),
                 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 {
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
             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 {
         @ViewBuilder private func carbsTextField() -> some View {
             HStack {
             HStack {
-                Text("Carbs").fontWeight(.semibold)
+                Text("Carbs")
                 Spacer()
                 Spacer()
                 TextFieldWithToolBar(
                 TextFieldWithToolBar(
                     text: $state.carbs,
                     text: $state.carbs,
@@ -133,7 +137,7 @@ extension Bolus {
                     previousTextField: { focusOnPreviousTextField(index: 1) },
                     previousTextField: { focusOnPreviousTextField(index: 1) },
                     nextTextField: { focusOnNextTextField(index: 1) }
                     nextTextField: { focusOnNextTextField(index: 1) }
                 ).focused($focusedField, equals: .carbs)
                 ).focused($focusedField, equals: .carbs)
-                    .onChange(of: state.carbs) { _ in
+                    .onChange(of: state.carbs) {
                         handleDebouncedInput()
                         handleDebouncedInput()
                     }
                     }
                 Text("g").foregroundColor(.secondary)
                 Text("g").foregroundColor(.secondary)
@@ -169,98 +173,100 @@ extension Bolus {
         var body: some View {
         var body: some View {
             ZStack(alignment: .center) {
             ZStack(alignment: .center) {
                 VStack {
                 VStack {
-                    Form {
+                    List {
                         Section {
                         Section {
-                            ForeCastChart(state: state, units: $state.units)
+                            ForecastChart(state: state, units: $state.units)
                                 .padding(.vertical)
                                 .padding(.vertical)
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
                         Section {
                         Section {
                             carbsTextField()
                             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 {
                                 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)
                         }.listRowBackground(Color.chart)
 
 
                         Section {
                         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 {
                             HStack {
-                                Text("Recommended Bolus")
+                                HStack {
+                                    Text("Recommendation")
+                                    Button(action: {
+                                        state.showInfo.toggle()
+                                    }, label: {
+                                        Image(systemName: "info.circle")
+                                    })
+                                        .foregroundStyle(.blue)
+                                        .buttonStyle(PlainButtonStyle())
+                                }
                                 Spacer()
                                 Spacer()
                                 Text(
                                 Text(
                                     formatter
                                     formatter
@@ -287,7 +293,7 @@ extension Bolus {
                                     previousTextField: { focusOnPreviousTextField(index: 4) },
                                     previousTextField: { focusOnPreviousTextField(index: 4) },
                                     nextTextField: { focusOnNextTextField(index: 4) }
                                     nextTextField: { focusOnNextTextField(index: 4) }
                                 ).focused($focusedField, equals: .bolus)
                                 ).focused($focusedField, equals: .bolus)
-                                    .onChange(of: state.amount) { _ in
+                                    .onChange(of: state.amount) {
                                         Task {
                                         Task {
                                             await state.updateForecasts()
                                             await state.updateForecasts()
                                         }
                                         }
@@ -296,14 +302,14 @@ extension Bolus {
                             }
                             }
 
 
                             HStack {
                             HStack {
-                                Text("External insulin")
+                                Text("External Insulin")
                                 Spacer()
                                 Spacer()
                                 Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
                                 Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
                         treatmentButton
                         treatmentButton
-                    }
+                    }.listSectionSpacing(20)
                 }
                 }
                 .blur(radius: state.waitForSuggestion ? 5 : 0)
                 .blur(radius: state.waitForSuggestion ? 5 : 0)
 
 
@@ -311,6 +317,8 @@ extension Bolus {
                     CustomProgressView(text: progressText.rawValue)
                     CustomProgressView(text: progressText.rawValue)
                 }
                 }
             }
             }
+            .padding(.top)
+            .ignoresSafeArea(edges: .top)
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .blur(radius: state.showInfo ? 3 : 0)
             .blur(radius: state.showInfo ? 3 : 0)
             .navigationTitle("Treatments")
             .navigationTitle("Treatments")
@@ -380,7 +388,10 @@ extension Bolus {
                     .frame(height: 35)
                     .frame(height: 35)
             }
             }
             .disabled(disableTaskButton)
             .disabled(disableTaskButton)
-            .listRowBackground(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
+            .listRowBackground(
+                limitExceeded ? Color(.systemRed) :
+                    Color(.systemBlue)
+            )
             .shadow(radius: 3)
             .shadow(radius: 3)
             .clipShape(RoundedRectangle(cornerRadius: 8))
             .clipShape(RoundedRectangle(cornerRadius: 8))
         }
         }

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

@@ -3,7 +3,7 @@ import CoreData
 import Foundation
 import Foundation
 import SwiftUI
 import SwiftUI
 
 
-struct ForeCastChart: View {
+struct ForecastChart: View {
     @StateObject var state: Bolus.StateModel
     @StateObject var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
     @Binding var units: GlucoseUnits
     @Binding var units: GlucoseUnits
@@ -35,69 +35,63 @@ struct ForeCastChart: View {
 
 
     var body: some View {
     var body: some View {
         VStack {
         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 {
             HStack {
-                Spacer()
                 Image(systemName: "arrow.right.circle")
                 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 {
                     HStack {
                         Text(
                         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 {
                 } else {
                     Text("---")
                     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 {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             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 {
             if !state.isSmoothingEnabled {
                 PointMark(
                 PointMark(
@@ -132,7 +137,7 @@ struct ForeCastChart: View {
                     y: .value("Value", glucoseToDisplay)
                     y: .value("Value", glucoseToDisplay)
                 )
                 )
                 .foregroundStyle(pointMarkColor)
                 .foregroundStyle(pointMarkColor)
-                .symbolSize(20)
+                .symbolSize(18)
             } else {
             } else {
                 PointMark(
                 PointMark(
                     x: .value("Time", item.date ?? Date(), unit: .second),
                     x: .value("Time", item.date ?? Date(), unit: .second),
@@ -140,7 +145,7 @@ struct ForeCastChart: View {
                 )
                 )
                 .symbol {
                 .symbol {
                     Image(systemName: "record.circle.fill")
                     Image(systemName: "record.circle.fill")
-                        .font(.system(size: 8))
+                        .font(.system(size: 6))
                         .bold()
                         .bold()
                         .foregroundStyle(pointMarkColor)
                         .foregroundStyle(pointMarkColor)
                 }
                 }
@@ -232,8 +237,8 @@ struct ForeCastChart: View {
         AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
         AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
             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
         AxisMarks(position: .trailing) { _ in
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
             AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
-            AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
+            AxisValueLabel().font(.caption2).foregroundStyle(Color.secondary)
         }
         }
     }
     }
 }
 }

+ 45 - 58
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -134,14 +134,15 @@ struct PopupView: View {
             Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
             Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
                 .gridCellAnchor(.leading)
                 .gridCellAnchor(.leading)
 
 
+            let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
             Text(
             Text(
-                state.isf.formatted() + " " + state.units
+                isf + " " + state.units
                     .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
                     .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
             ).gridCellAnchor(.leading)
             ).gridCellAnchor(.leading)
-            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+
+            let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
             Text(
             Text(
-                target
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                target +
                     " " + state.units.rawValue
                     " " + state.units.rawValue
             ).gridCellAnchor(.leading)
             ).gridCellAnchor(.leading)
         }
         }
@@ -149,28 +150,25 @@ struct PopupView: View {
 
 
     var calcGlucoseFirstRow: some View {
     var calcGlucoseFirstRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
-            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+            let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
+            let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
 
 
             Text("Glucose:").foregroundColor(.secondary)
             Text("Glucose:").foregroundColor(.secondary)
 
 
-            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
+            let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
+                .description
             let firstRow = currentBG
             let firstRow = currentBG
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-
                 + " - " +
                 + " - " +
                 target
                 target
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
                 + " = " +
                 + " = " +
                 targetDifference
                 targetDifference
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
 
 
             Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
             Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
                 .gridColumnAlignment(.leading)
                 .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
                 Text(
                 Text(
-                    self.insulinRounder(state.targetDifferenceInsulin).formatted()
+                    self.insulinFormatter(state.targetDifferenceInsulin)
                 )
                 )
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
@@ -180,24 +178,18 @@ struct PopupView: View {
 
 
     var calcGlucoseSecondRow: some View {
     var calcGlucoseSecondRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+            let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
             Text(
             Text(
                 currentBG
                 currentBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    " " +
+                    + " " +
                     state.units.rawValue
                     state.units.rawValue
             )
             )
 
 
-            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
-            let secondRow = targetDifference
-                .formatted(
-                    .number.grouping(.never).rounded()
-                        .precision(.fractionLength(fractionDigits))
-                )
-                + " / " +
-                (state.units == .mgdL ? state.isf : state.isf.asMmolL).formatted()
-                + " ≈ " +
-                self.insulinRounder(state.targetDifferenceInsulin).formatted()
+            let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
+                .description
+            let secondRow = targetDifference + " / " +
+                (state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description)
+                .description + " ≈ " + self.insulinFormatter(state.targetDifferenceInsulin)
 
 
             Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
             Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
 
 
@@ -221,13 +213,13 @@ struct PopupView: View {
             HStack {
             HStack {
                 Text("IOB:").foregroundColor(.secondary)
                 Text("IOB:").foregroundColor(.secondary)
                 Text(
                 Text(
-                    self.insulinRounder(state.iob).formatted()
+                    self.insulinFormatter(state.iob)
                 )
                 )
             }
             }
 
 
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
-            let iobFormatted = self.insulinRounder(state.iob).formatted()
+            let iobFormatted = self.insulinFormatter(state.iob)
             HStack {
             HStack {
                 Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
                 Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -253,14 +245,14 @@ struct PopupView: View {
                     + " / " +
                     + " / " +
                     state.carbRatio.formatted()
                     state.carbRatio.formatted()
                     + " ≈ " +
                     + " ≈ " +
-                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                    self.insulinFormatter(state.wholeCobInsulin)
             )
             )
             .foregroundColor(.secondary)
             .foregroundColor(.secondary)
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
                 Text(
                 Text(
-                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                    self.insulinFormatter(state.wholeCobInsulin)
                 )
                 )
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
@@ -283,25 +275,19 @@ struct PopupView: View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
             Text("Delta:").foregroundColor(.secondary)
             Text("Delta:").foregroundColor(.secondary)
 
 
-            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
+            let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
+
+            let fifteenMinInsulinFormatted = self.insulinFormatter(state.fifteenMinInsulin)
+
             Text(
             Text(
-                deltaBG
-                    .formatted(
-                        .number.grouping(.never).rounded()
-                            .precision(.fractionLength(fractionDigits))
-                    )
-                    + " / " +
-                    state.isf.formatted()
-                    + " ≈ " +
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
+                deltaBG + " / " + isf + " ≈ " + fifteenMinInsulinFormatted
             )
             )
             .foregroundColor(.secondary)
             .foregroundColor(.secondary)
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
-                Text(
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
-                )
+                Text(fifteenMinInsulinFormatted)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
                 .gridColumnAlignment(.trailing)
                 .gridColumnAlignment(.trailing)
@@ -310,13 +296,10 @@ struct PopupView: View {
 
 
     var calcDeltaFormulaRow: some View {
     var calcDeltaFormulaRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
             Text(
             Text(
                 deltaBG
                 deltaBG
-                    .formatted(
-                        .number.grouping(.never).rounded()
-                            .precision(.fractionLength(fractionDigits))
-                    ) + " " +
+                    + " " +
                     state.units.rawValue
                     state.units.rawValue
             )
             )
 
 
@@ -334,7 +317,7 @@ struct PopupView: View {
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
 
 
             HStack {
             HStack {
-                Text(self.insulinRounder(state.wholeCalc).formatted())
+                Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.gridColumnAlignment(.trailing)
             }.gridColumnAlignment(.trailing)
@@ -350,7 +333,7 @@ struct PopupView: View {
             Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
             HStack {
             HStack {
-                Text("+" + self.insulinRounder(state.superBolusInsulin).formatted())
+                Text("+" + self.insulinFormatter(state.superBolusInsulin))
                     .foregroundStyle(Color.loopRed)
                     .foregroundStyle(Color.loopRed)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.gridColumnAlignment(.trailing)
             }.gridColumnAlignment(.trailing)
@@ -379,7 +362,7 @@ struct PopupView: View {
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
                     // endif fatty meal is chosen
                     // endif fatty meal is chosen
 
 
-                    + Text(self.insulinRounder(state.wholeCalc).formatted())
+                    + Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
 
 
                     // if superbolus is chosen
                     // if superbolus is chosen
@@ -389,7 +372,7 @@ struct PopupView: View {
                     + Text(state.useSuperBolus ? " + " : "")
                     + Text(state.useSuperBolus ? " + " : "")
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
 
 
-                    + Text(state.useSuperBolus ? state.superBolusInsulin.formatted() : "")
+                    + Text(state.useSuperBolus ? self.insulinFormatter(state.superBolusInsulin) : "")
                     .foregroundColor(.loopRed)
                     .foregroundColor(.loopRed)
                     // endif superbolus is chosen
                     // endif superbolus is chosen
 
 
@@ -399,7 +382,7 @@ struct PopupView: View {
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
-                Text(self.insulinRounder(state.insulinCalculated).formatted())
+                Text(self.insulinFormatter(state.insulinCalculated))
                     .fontWeight(.bold)
                     .fontWeight(.bold)
                     .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
                     .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -447,9 +430,17 @@ struct PopupView: View {
         }
         }
     }
     }
 
 
-    private func insulinRounder(_ value: Decimal) -> Decimal {
+    private func insulinFormatter(_ value: Decimal) -> String {
         let toRound = NSDecimalNumber(decimal: value).doubleValue
         let toRound = NSDecimalNumber(decimal: value).doubleValue
-        return Decimal(floor(100 * toRound) / 100)
+        let roundedValue = Decimal(floor(100 * toRound) / 100)
+
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumFractionDigits = 2
+        formatter.maximumFractionDigits = 2
+        formatter.locale = Locale.current // Uses the user's locale
+
+        return formatter.string(from: roundedValue as NSNumber) ?? String(format: "%.2f", toRound)
     }
     }
 
 
     struct DividerDouble: View {
     struct DividerDouble: View {
@@ -476,7 +467,3 @@ struct PopupView: View {
         }
         }
     }
     }
 }
 }
-
-// #Preview {
-//    PopupView()
-// }

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

@@ -1,5 +1,6 @@
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import HealthKit
 import SwiftUI
 import SwiftUI
 
 
 enum DataTable {
 enum DataTable {
@@ -218,6 +219,10 @@ enum DataTable {
 }
 }
 
 
 protocol DataTableProvider: Provider {
 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 CoreData
 import Foundation
 import Foundation
+import HealthKit
 
 
 extension DataTable {
 extension DataTable {
     final class Provider: BaseProvider, DataTableProvider {
     final class Provider: BaseProvider, DataTableProvider {
@@ -14,52 +15,53 @@ extension DataTable {
         }
         }
 
 
         func deleteCarbsFromNightscout(withID id: String) {
         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)
+        }
     }
     }
 }
 }

+ 122 - 64
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -1,4 +1,5 @@
 import CoreData
 import CoreData
+import HealthKit
 import SwiftUI
 import SwiftUI
 
 
 extension DataTable {
 extension DataTable {
@@ -10,6 +11,7 @@ extension DataTable {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var healthKitManager: HealthKitManager!
         @Injected() var healthKitManager: HealthKitManager!
+        @Injected() var carbsStorage: CarbsStorage!
 
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
@@ -36,19 +38,26 @@ extension DataTable {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
             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 {
             Task {
                 await deleteGlucose(treatmentObjectID)
                 await deleteGlucose(treatmentObjectID)
             }
             }
         }
         }
 
 
         func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
         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()
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
             taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteGlucose"
+            taskContext.transactionAuthor = "deleteGlucoseFromServices"
 
 
             await taskContext.perform {
             await taskContext.perform {
                 do {
                 do {
@@ -59,107 +68,123 @@ extension DataTable {
                         return
                         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 {
                 } catch {
                     debugPrint(
                     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
         // 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 {
             Task {
                 await deleteCarbs(treatmentObjectID)
                 await deleteCarbs(treatmentObjectID)
-                carbEntryDeleted = true
-                waitForSuggestion = true
+
+                await MainActor.run {
+                    carbEntryDeleted = true
+                    waitForSuggestion = true
+                }
             }
             }
         }
         }
 
 
         func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
         func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+            // Delete from Apple Health/Tidepool
+            await deleteCarbsFromServices(treatmentObjectID)
+
+            // Delete 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()
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
             taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteCarbs"
+            taskContext.transactionAuthor = "deleteCarbsFromServices"
 
 
             var carbEntry: CarbEntryStored?
             var carbEntry: CarbEntryStored?
 
 
+            // Delete carbs or FPUs from Nightscout
             await taskContext.perform {
             await taskContext.perform {
                 do {
                 do {
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     guard let carbEntry = carbEntry else {
                     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
                         return
                     }
                     }
 
 
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
-                        // Delete FPUs from Nightscout
+                        // Delete Fat and Protein entries from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
 
 
-                        // fetch request for all carb entries with the same id
-                        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                        fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
-
-                        // NSBatchDeleteRequest
-                        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
-                        deleteRequest.resultType = .resultTypeCount
+                        // Delete Fat and Protein entries from Apple Health
+                        let healthObjectsToDelete: [HKSampleType?] = [
+                            AppleHealthConfig.healthFatObject,
+                            AppleHealthConfig.healthProteinObject
+                        ]
 
 
-                        // execute the batch delete request
-                        let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
-                        debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
-
-                        Foundation.NotificationCenter.default.post(name: .didPerformBatchDelete, object: nil)
+                        for sampleType in healthObjectsToDelete {
+                            if let validSampleType = sampleType {
+                                self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
+                            }
+                        }
                     } else {
                     } else {
                         // Delete carbs from Nightscout
                         // 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
+                            )
                         }
                         }
-
-                        // Now delete carbs also from the Database
-                        taskContext.delete(carbEntry)
-
-                        guard taskContext.hasChanges else { return }
-                        try taskContext.save()
-
-                        debugPrint(
-                            "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
-                        )
                     }
                     }
 
 
                 } catch {
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+                    debugPrint(
+                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
+                    )
                 }
                 }
             }
             }
-
-            // Perform a determine basal sync to update cob
-            await apsManager.determineBasalSync()
         }
         }
 
 
         // Insulin deletion from history
         // 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 {
             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 {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 let authenticated = try await unlockmanager.unlock()
 
 
@@ -168,14 +193,14 @@ extension DataTable {
                     return
                     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 {
             } catch {
                 debugPrint(
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
@@ -183,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() {
         func addManualGlucose() {
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let glucoseAsInt = Int(glucose)
             let glucoseAsInt = Int(glucose)
@@ -195,6 +251,8 @@ extension DataTable {
                 newItem.glucose = Int16(glucoseAsInt)
                 newItem.glucose = Int16(glucoseAsInt)
                 newItem.isManual = true
                 newItem.isManual = true
                 newItem.isUploadedToNS = false
                 newItem.isUploadedToNS = false
+                newItem.isUploadedToHealth = false
+                newItem.isUploadedToTidepool = false
 
 
                 do {
                 do {
                     guard self.coredataContext.hasChanges else { return }
                     guard self.coredataContext.hasChanges else { return }

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -70,7 +70,7 @@ extension DataTable {
                 formatter.minimumFractionDigits = 0
                 formatter.minimumFractionDigits = 0
                 formatter.maximumFractionDigits = 1
                 formatter.maximumFractionDigits = 1
             }
             }
-            formatter.roundingMode = .down
+            formatter.roundingMode = .halfUp
             return formatter
             return formatter
         }
         }
 
 

Разница между файлами не показана из-за своего большого размера
+ 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
             useAppleHealth = settingsManager.settings.useAppleHealth
 
 
-            needShowInformationTextForSetPermissions = healthKitManager.areAllowAllPermissions
+            needShowInformationTextForSetPermissions = healthKitManager.hasGrantedFullWritePermissions
 
 
             subscribeSetting(\.useAppleHealth, on: $useAppleHealth) {
             subscribeSetting(\.useAppleHealth, on: $useAppleHealth) {
                 useAppleHealth = $0
                 useAppleHealth = $0
@@ -26,20 +26,22 @@ extension AppleHealthKit {
                     return
                     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()
                 }
                 }
             }
             }
         }
         }

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

@@ -53,5 +53,11 @@ extension Home {
             storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
             storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
                 ?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
                 ?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
         }
         }
+
+        func getBGTarget() async -> BGTargets {
+            await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+                ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+                ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+        }
     }
     }
 }
 }

+ 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))
+    }
+}

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

@@ -0,0 +1,98 @@
+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")
+            }
+        }
+    }
+}

+ 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)"
+            )
+        }
+    }
+}

+ 116 - 528
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -13,6 +13,7 @@ extension Home {
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var determinationStorage: DeterminationStorage!
         @Injected() var determinationStorage: DeterminationStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var carbsStorage: CarbsStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         private(set) var filteredHours = 24
         @Published var manualGlucose: [BloodGlucose] = []
         @Published var manualGlucose: [BloodGlucose] = []
@@ -48,7 +49,9 @@ extension Home {
         @Published var maxValue: Decimal = 1.2
         @Published var maxValue: Decimal = 1.2
         @Published var lowGlucose: Decimal = 70
         @Published var lowGlucose: Decimal = 70
         @Published var highGlucose: Decimal = 180
         @Published var highGlucose: Decimal = 180
+        @Published var currentGlucoseTarget: Decimal = 100
         @Published var overrideUnit: Bool = false
         @Published var overrideUnit: Bool = false
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var displayXgridLines: Bool = false
         @Published var displayXgridLines: Bool = false
         @Published var displayYgridLines: Bool = false
         @Published var displayYgridLines: Bool = false
         @Published var thresholdLines: Bool = false
         @Published var thresholdLines: Bool = false
@@ -87,15 +90,39 @@ 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 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
         @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 batteryFetchContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
-        private var coreDataObserver: CoreDataObserver?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
-            coreDataObserver = CoreDataObserver()
+            coreDataPublisher =
+                changedObjectsOnManagedObjectContextDidSavePublisher()
+                    .receive(on: DispatchQueue.global(qos: .background))
+                    .share()
+                    .eraseToAnyPublisher()
+
+            registerSubscribers()
+            registerHandlers()
 
 
             // Parallelize Setup functions
             // Parallelize Setup functions
             setupHomeViewConcurrently()
             setupHomeViewConcurrently()
@@ -105,12 +132,6 @@ extension Home {
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
-                        self.setupNotification()
-                    }
-                    group.addTask {
-                        self.registerHandlers()
-                    }
-                    group.addTask {
                         self.setupGlucoseArray()
                         self.setupGlucoseArray()
                     }
                     }
                     group.addTask {
                     group.addTask {
@@ -165,43 +186,62 @@ extension Home {
             }
             }
         }
         }
 
 
+        // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
+        private func registerSubscribers() {
+            glucoseStorage.updatePublisher
+                .receive(on: DispatchQueue.global(qos: .background))
+                .sink { [weak self] _ in
+                    guard let self = self else { return }
+                    self.setupGlucoseArray()
+                }
+                .store(in: &subscriptions)
+
+            carbsStorage.updatePublisher
+                .receive(on: DispatchQueue.global(qos: .background))
+                .sink { [weak self] _ in
+                    guard let self = self else { return }
+                    self.setupFPUsArray()
+                }
+                .store(in: &subscriptions)
+        }
+
         private func registerHandlers() {
         private func registerHandlers() {
-            coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupDeterminationsArray()
                 self.setupDeterminationsArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupCarbsArray()
                 self.setupCarbsArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupInsulinArray()
                 self.setupInsulinArray()
                 self.setupLastBolus()
                 self.setupLastBolus()
                 self.displayPumpStatusHighlightMessage()
                 self.displayPumpStatusHighlightMessage()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "OpenAPS_Battery") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OpenAPS_Battery").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupBatteryArray()
                 self.setupBatteryArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupOverrides()
                 self.setupOverrides()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupOverrideRunStored()
                 self.setupOverrideRunStored()
-            }
+            }.store(in: &subscriptions)
         }
         }
 
 
         private func registerObservers() {
         private func registerObservers() {
@@ -288,6 +328,7 @@ extension Home {
             manualTempBasal = apsManager.isManualTempBasal
             manualTempBasal = apsManager.isManualTempBasal
             setupCurrentTempTarget()
             setupCurrentTempTarget()
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            glucoseColorScheme = settingsManager.settings.glucoseColorScheme
             maxValue = settingsManager.preferences.autosensMax
             maxValue = settingsManager.preferences.autosensMax
             lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
             lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
             highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
             highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
@@ -453,6 +494,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() {
         func openCGM() {
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
         }
         }
@@ -496,7 +585,11 @@ extension Home.StateModel:
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
         lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
         lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
         highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
         highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+        Task {
+            await getCurrentGlucoseTarget()
+        }
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
+        glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         displayXgridLines = settingsManager.settings.xGridLines
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
         thresholdLines = settingsManager.settings.rulerMarks
@@ -563,508 +656,3 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // TODO:
         // TODO:
     }
     }
 }
 }
-
-// MARK: - Setup Core Data observation
-
-extension Home.StateModel {
-    /// listens for the notifications sent when the managedObjectContext has saved!
-    func setupNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-
-        /// custom notification that is sent when a batch delete of fpus is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchDelete),
-            name: .didPerformBatchDelete,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        setupFPUsArray()
-        setupGlucoseArray()
-    }
-
-    @objc private func handleBatchDelete() {
-        setupFPUsArray()
-    }
-}
-
-// 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
-    }
-
-    @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)
         .backport.chartXSelection(value: $selection)
         .chartXAxis { basalChartXAxis }
         .chartXAxis { basalChartXAxis }
         .chartYAxis { cobChartYAxis }
         .chartYAxis { cobChartYAxis }
-        .chartYScale(domain: minValueCobChart ... maxValueCobChart)
+        .chartYScale(domain: state.minValueCobChart ... state.maxValueCobChart)
     }
     }
 
 
     func drawCOB(dummy: Bool) -> some ChartContent {
     func drawCOB(dummy: Bool) -> some ChartContent {

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

@@ -8,9 +8,28 @@ extension MainChartView {
         Chart {
         Chart {
             /// high and low threshold lines
             /// high and low threshold lines
             if thresholdLines {
             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]))
                     .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]))
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
             }
             }
         }
         }
@@ -21,7 +40,10 @@ extension MainChartView {
         .chartXScale(domain: startMarker ... endMarker)
         .chartXScale(domain: startMarker ... endMarker)
         .chartXAxis(.hidden)
         .chartXAxis(.hidden)
         .chartYAxis { mainChartYAxis }
         .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)
         .chartLegend(.hidden)
     }
     }
 
 
@@ -48,7 +70,7 @@ extension MainChartView {
         .chartXAxis(.hidden)
         .chartXAxis(.hidden)
         .chartYAxis { cobChartYAxis }
         .chartYAxis { cobChartYAxis }
         .chartYAxis(.hidden)
         .chartYAxis(.hidden)
-        .chartYScale(domain: minValueCobChart ... maxValueCobChart)
+        .chartYScale(domain: state.minValueCobChart ... state.maxValueCobChart)
         .chartLegend(.hidden)
         .chartLegend(.hidden)
     }
     }
 }
 }

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

@@ -7,7 +7,9 @@ struct GlucoseChartView: ChartContent {
     let units: GlucoseUnits
     let units: GlucoseUnits
     let highGlucose: Decimal
     let highGlucose: Decimal
     let lowGlucose: Decimal
     let lowGlucose: Decimal
+    let currentGlucoseTarget: Decimal
     let isSmoothingEnabled: Bool
     let isSmoothingEnabled: Bool
+    let glucoseColorScheme: GlucoseColorScheme
 
 
     var body: some ChartContent {
     var body: some ChartContent {
         drawGlucoseChart()
         drawGlucoseChart()
@@ -16,9 +18,19 @@ struct GlucoseChartView: ChartContent {
     private func drawGlucoseChart() -> some ChartContent {
     private func drawGlucoseChart() -> some ChartContent {
         ForEach(glucoseData) { item in
         ForEach(glucoseData) { item in
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             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 {
             if !isSmoothingEnabled {
                 PointMark(
                 PointMark(

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

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

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

@@ -12,7 +12,9 @@ struct MainChartView: View {
     @Binding var tempTargets: [TempTarget]
     @Binding var tempTargets: [TempTarget]
     @Binding var highGlucose: Decimal
     @Binding var highGlucose: Decimal
     @Binding var lowGlucose: Decimal
     @Binding var lowGlucose: Decimal
+    @Binding var currentGlucoseTarget: Decimal
     @Binding var screenHours: Int16
     @Binding var screenHours: Int16
+    @Binding var glucoseColorScheme: GlucoseColorScheme
     @Binding var displayXgridLines: Bool
     @Binding var displayXgridLines: Bool
     @Binding var displayYgridLines: Bool
     @Binding var displayYgridLines: Bool
     @Binding var thresholdLines: Bool
     @Binding var thresholdLines: Bool
@@ -25,13 +27,9 @@ struct MainChartView: View {
     @State var startMarker =
     @State var startMarker =
         Date(timeIntervalSinceNow: TimeInterval(hours: -24))
         Date(timeIntervalSinceNow: TimeInterval(hours: -24))
     @State var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
     @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 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
     @State var mainChartHasInitialized = false
 
 
     let now = Date.now
     let now = Date.now
@@ -104,29 +102,22 @@ struct MainChartView: View {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
                         }
                         .onChange(of: state.glucoseFromPersistence.last?.glucose) { _ in
                         .onChange(of: state.glucoseFromPersistence.last?.glucose) { _ in
-                            updateStartEndMarkers()
-                            yAxisChartData()
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             scroller.scrollTo("MainChart", anchor: .trailing)
+                            updateStartEndMarkers()
                         }
                         }
                         .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) { _ in
                         .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) { _ in
-                            yAxisChartDataCobChart()
-                            yAxisChartDataIobChart()
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
                         }
                         .onChange(of: units) { _ in
                         .onChange(of: units) { _ in
-                            yAxisChartData()
-                            yAxisChartDataCobChart()
-                            yAxisChartDataIobChart()
+                            // TODO: - Refactor this to only update the Y Axis Scale
+                            state.setupGlucoseArray()
                         }
                         }
                         .onAppear {
                         .onAppear {
                             if !mainChartHasInitialized {
                             if !mainChartHasInitialized {
+                                scroller.scrollTo("MainChart", anchor: .trailing)
                                 updateStartEndMarkers()
                                 updateStartEndMarkers()
-                                yAxisChartData()
-                                yAxisChartDataCobChart()
-                                yAxisChartDataIobChart()
                                 calculateTempBasalsInBackground()
                                 calculateTempBasalsInBackground()
                                 mainChartHasInitialized = true
                                 mainChartHasInitialized = true
-                                scroller.scrollTo("MainChart", anchor: .trailing)
                             }
                             }
                         }
                         }
                     }
                     }
@@ -152,7 +143,9 @@ extension MainChartView {
                     units: state.units,
                     units: state.units,
                     highGlucose: state.highGlucose,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
                     lowGlucose: state.lowGlucose,
-                    isSmoothingEnabled: state.isSmoothingEnabled
+                    currentGlucoseTarget: state.currentGlucoseTarget,
+                    isSmoothingEnabled: state.isSmoothingEnabled,
+                    glucoseColorScheme: state.glucoseColorScheme
                 )
                 )
 
 
                 InsulinView(
                 InsulinView(
@@ -166,7 +159,7 @@ extension MainChartView {
                     units: state.units,
                     units: state.units,
                     carbData: state.carbsFromPersistence,
                     carbData: state.carbsFromPersistence,
                     fpuData: state.fpusFromPersistence,
                     fpuData: state.fpusFromPersistence,
-                    minValue: minValue
+                    minValue: state.minYAxisValue
                 )
                 )
 
 
                 OverrideView(
                 OverrideView(
@@ -181,7 +174,7 @@ extension MainChartView {
                     minForecast: state.minForecast,
                     minForecast: state.minForecast,
                     maxForecast: state.maxForecast,
                     maxForecast: state.maxForecast,
                     units: state.units,
                     units: state.units,
-                    maxValue: maxValue,
+                    maxValue: state.maxYAxisValue,
                     forecastDisplayType: state.forecastDisplayType
                     forecastDisplayType: state.forecastDisplayType
                 )
                 )
 
 
@@ -241,7 +234,10 @@ extension MainChartView {
             .chartYAxis { mainChartYAxis }
             .chartYAxis { mainChartYAxis }
             .chartYAxis(.hidden)
             .chartYAxis(.hidden)
             .backport.chartXSelection(value: $selection)
             .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)
             .backport.chartForegroundStyleScale(state: state)
         }
         }
     }
     }
@@ -256,13 +252,20 @@ extension MainChartView {
                         .font(.body).bold()
                         .font(.body).bold()
                 }.font(.body).padding(.bottom, 5)
                 }.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 {
                 HStack {
                     Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
                     Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
                         .bold()
                         .bold()
                         + Text(" \(units.rawValue)")
                         + Text(" \(units.rawValue)")
                 }.foregroundStyle(
                 }.foregroundStyle(
-                    glucoseToShow < lowGlucose ? Color
-                        .red : (glucoseToShow > highGlucose ? Color.orange : Color.primary)
+                    Color(glucoseColor)
                 ).font(.body)
                 ).font(.body)
 
 
                 if let selectedIOBValue, let iob = selectedIOBValue.iob {
                 if let selectedIOBValue, let iob = selectedIOBValue.iob {
@@ -411,84 +414,6 @@ extension MainChartView {
             .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
             .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
             dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : 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 {
 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 lowGlucose: Decimal
     @Binding var highGlucose: Decimal
     @Binding var highGlucose: Decimal
     @Binding var cgmAvailable: Bool
     @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
     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 {
                         if let glucoseValue = glucose.last?.glucose {
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                                 .formattedAsMmolL
                                 .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
                                 glucoseValue == 400 ? "HIGH" : displayGlucose
                             )
                             )
                             .font(.system(size: 40, weight: .bold, design: .rounded))
                             .font(.system(size: 40, weight: .bold, design: .rounded))
-                            .foregroundColor(alarm == nil ? glucoseDisplayColor : .loopRed)
+                            .foregroundStyle(glucoseDisplayColor)
                         } else {
                         } else {
-                            Text("--")
+                            return Text("--")
                                 .font(.system(size: 40, weight: .bold, design: .rounded))
                                 .font(.system(size: 40, weight: .bold, design: .rounded))
-                                .foregroundColor(.secondary)
+                                .foregroundStyle(.secondary)
                         }
                         }
                     }
                     }
                     HStack {
                     HStack {
@@ -99,18 +120,18 @@ struct CurrentGlucoseView: View {
                                     NSLocalizedString("min", comment: "Short form for minutes") + " "
                                     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(
                         Text(
                             delta
                             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)
                     }.frame(alignment: .top)
                 }
                 }
             }
             }
-            .onChange(of: glucose.last?.directionEnum) { newDirection in
+            .onChange(of: glucose.last?.directionEnum) {
                 withAnimation {
                 withAnimation {
-                    switch newDirection {
+                    switch glucose.last?.directionEnum {
                     case .doubleUp,
                     case .doubleUp,
                          .singleUp,
                          .singleUp,
                          .tripleUp:
                          .tripleUp:
@@ -160,34 +181,27 @@ struct CurrentGlucoseView: View {
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
         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 {
 struct Triangle: Shape {

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

@@ -136,6 +136,8 @@ extension Home {
                 lowGlucose: $state.lowGlucose,
                 lowGlucose: $state.lowGlucose,
                 highGlucose: $state.highGlucose,
                 highGlucose: $state.highGlucose,
                 cgmAvailable: $state.cgmAvailable,
                 cgmAvailable: $state.cgmAvailable,
+                currentGlucoseTarget: $state.currentGlucoseTarget,
+                glucoseColorScheme: $state.glucoseColorScheme,
                 glucose: state.latestTwoGlucoseValues
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
             ).scaleEffect(0.9)
                 .onTapGesture {
                 .onTapGesture {
@@ -346,7 +348,9 @@ extension Home {
                     tempTargets: $state.tempTargets,
                     tempTargets: $state.tempTargets,
                     highGlucose: $state.highGlucose,
                     highGlucose: $state.highGlucose,
                     lowGlucose: $state.lowGlucose,
                     lowGlucose: $state.lowGlucose,
+                    currentGlucoseTarget: $state.currentGlucoseTarget,
                     screenHours: $state.hours,
                     screenHours: $state.hours,
+                    glucoseColorScheme: $state.glucoseColorScheme,
                     displayXgridLines: $state.displayXgridLines,
                     displayXgridLines: $state.displayXgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     thresholdLines: $state.thresholdLines,
                     thresholdLines: $state.thresholdLines,

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

@@ -326,12 +326,17 @@ extension NightscoutConfig {
             if glucose.isNotEmpty {
             if glucose.isNotEmpty {
                 await MainActor.run {
                 await MainActor.run {
                     self.backfilling = false
                     self.backfilling = false
-                    self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
-                    self.glucoseStorage.storeGlucose(glucose)
+                }
+
+                glucoseStorage.storeGlucose(glucose)
+
+                Task.detached {
+                    await self.healthKitManager.uploadGlucose()
                 }
                 }
             } else {
             } else {
                 await MainActor.run {
                 await MainActor.run {
                     self.backfilling = false
                     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(
                     Section(
                         header: Text("Nightscout Integration"),
                         header: Text("Nightscout Integration"),
                         content: {
                         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("Upload", destination: NightscoutUploadView(state: state))
                             NavigationLink("Fetch & Remote Control", destination: NightscoutFetchView(state: state))
                             NavigationLink("Fetch & Remote Control", destination: NightscoutFetchView(state: state))
                         }
                         }

Разница между файлами не показана из-за своего большого размера
+ 47 - 47
FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift


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

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

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

@@ -99,6 +99,6 @@ extension Settings.StateModel: ServiceOnboardingDelegate {
 extension Settings.StateModel: CompletionDelegate {
 extension Settings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
         setupTidepool = false
         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:
                 content:
                 {
                 {
                     VStack {
                     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) {
                         HStack(alignment: .top) {
                             Text("You can connect Trio to seamlessly upload and manage your diabetes data on Tidepool.")
                             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"]
                 propertiesToFetch: ["glucose", "objectID"]
             )
             )
 
 
-            guard let fetchedResults = results as? [[String: Any]] else { return [] }
-
             return await context.perform {
             return await context.perform {
+                guard let fetchedResults = results as? [[String: Any]] else { return [] }
+
                 return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
                 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 low: Decimal = 70
         @Published var high: Decimal = 180
         @Published var high: Decimal = 180
         @Published var hours: Decimal = 6
         @Published var hours: Decimal = 6
+        @Published var dynamicGlucoseColor = false
         @Published var xGridLines = false
         @Published var xGridLines = false
         @Published var yGridLines: Bool = false
         @Published var yGridLines: Bool = false
         @Published var oneDimensionalGraph = false
         @Published var oneDimensionalGraph = false
@@ -25,6 +26,7 @@ extension StatConfig {
             self.units = units
             self.units = units
 
 
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
+            subscribeSetting(\.dynamicGlucoseColor, on: $dynamicGlucoseColor) { dynamicGlucoseColor = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $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 {
         var body: some View {
             Form {
             Form {
                 Section {
                 Section {
+                    Toggle("Use Dynamic BG Color", isOn: $state.dynamicGlucoseColor)
                     Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
                     Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)
                     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 totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var carbsRequiredThreshold: Decimal = 0
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
@@ -41,6 +42,8 @@ extension UserInterfaceSettings {
                 \.carbsRequiredThreshold,
                 \.carbsRequiredThreshold,
                 on: $carbsRequiredThreshold
                 on: $carbsRequiredThreshold
             ) { carbsRequiredThreshold = $0 }
             ) { carbsRequiredThreshold = $0 }
+
+            subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
         }
         }
     }
     }
 }
 }

+ 76 - 0
FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -15,6 +15,8 @@ extension UserInterfaceSettings {
         @State private var displayPickerLowThreshold: Bool = false
         @State private var displayPickerLowThreshold: Bool = false
         @State private var displayPickerHighThreshold: Bool = false
         @State private var displayPickerHighThreshold: Bool = false
 
 
+        @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
         var color: LinearGradient {
             colorScheme == .dark ? LinearGradient(
             colorScheme == .dark ? LinearGradient(
@@ -54,6 +56,80 @@ extension UserInterfaceSettings {
         var body: some View {
         var body: some View {
             Form {
             Form {
                 Section(
                 Section(
+                    header: Text("General Appearance"),
+                    content: {
+                        VStack {
+                            Picker(
+                                selection: $colorSchemePreference,
+                                label: Text("Trio Color Scheme")
+                            ) {
+                                ForEach(ColorSchemeOption.allCases) { selection in
+                                    Text(selection.displayName).tag(selection)
+                                }
+                            }.padding(.top)
+
+                            HStack(alignment: .top) {
+                                Text(
+                                    "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                )
+                                .font(.footnote)
+                                .foregroundColor(.secondary)
+                                .lineLimit(nil)
+                                Spacer()
+                                Button(
+                                    action: {
+                                        hintLabel = "Color Scheme Preference"
+                                        selectedVerboseHint = "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 {
+                    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"),
                     header: Text("Home View Settings"),
                     content: {
                     content: {
                         VStack {
                         VStack {

+ 26 - 24
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -19,7 +19,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     private var glucoseFormatter: NumberFormatter {
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
@@ -58,12 +59,18 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         setupCurrentCalendar()
         setupCurrentCalendar()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         Task {
         Task {
             await createEvent()
             await createEvent()
         }
         }
-        coreDataObserver = CoreDataObserver()
         registerHandlers()
         registerHandlers()
-        setupGlucoseNotification()
+        registerSubscribers()
     }
     }
 
 
     let backgroundContext = CoreDataStack.shared.newTaskContext()
     let backgroundContext = CoreDataStack.shared.newTaskContext()
@@ -79,28 +86,24 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.createEvent()
                 await self.createEvent()
             }
             }
-        }
-    }
-
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
+        }.store(in: &subscriptions)
     }
     }
 
 
-    @objc private func handleBatchInsert() {
-        Task {
-            await createEvent()
-        }
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.createEvent()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     func requestAccessIfNeeded() async -> Bool {
     func requestAccessIfNeeded() async -> Bool {
@@ -180,9 +183,9 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
         )
         )
 
 
-        guard let fetchedResults = results as? [[String: Any]] /* , !fetchedResults.isEmpty */ else { return nil }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else { return nil }
+
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
         }
         }
     }
     }
@@ -197,9 +200,9 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["objectID", "glucose"]
             propertiesToFetch: ["objectID", "glucose"]
         )
         )
 
 
-        guard let fetchedResults = results as? [[String: Any]] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else { return [] }
+
             return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
             return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
         }
         }
     }
     }
@@ -252,7 +255,6 @@ final class BaseCalendarManager: CalendarManager, Injectable {
                     settingsManager.settings.units == .mmolL ? Int(lastGlucoseValue)
                     settingsManager.settings.units == .mmolL ? Int(lastGlucoseValue)
                         .asMmolL : Decimal(lastGlucoseValue)
                         .asMmolL : Decimal(lastGlucoseValue)
                 ) as NSNumber)!
                 ) as NSNumber)!
-            debugPrint("\(DebuggingIdentifiers.failed) glucose text: \(glucoseText)")
 
 
             let directionText = lastGlucoseObject.directionEnum?.symbol ?? "↔︎"
             let directionText = lastGlucoseObject.directionEnum?.symbol ?? "↔︎"
 
 

Разница между файлами не показана из-за своего большого размера
+ 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
             fetchLimit: 72
         )
         )
 
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return
-        }
-
         await context.perform {
         await context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return
+            }
+
             self.glucoseFromPersistence = glucoseResults.map {
             self.glucoseFromPersistence = glucoseResults.map {
                 GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum)
                 GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum)
             }
             }
@@ -36,11 +36,11 @@ extension LiveActivityBridge {
             propertiesToFetch: ["iob", "cob"]
             propertiesToFetch: ["iob", "cob"]
         )
         )
 
 
-        guard let determinationResults = results as? [[String: Any]] else {
-            return
-        }
-
         await context.perform {
         await context.perform {
+            guard let determinationResults = results as? [[String: Any]] else {
+                return
+            }
+
             self.determination = determinationResults.first.map {
             self.determination = determinationResults.first.map {
                 DeterminationData(
                 DeterminationData(
                     cob: ($0["cob"] as? Int) ?? 0,
                     cob: ($0["cob"] as? Int) ?? 0,
@@ -61,11 +61,11 @@ extension LiveActivityBridge {
             propertiesToFetch: ["enabled"]
             propertiesToFetch: ["enabled"]
         )
         )
 
 
-        guard let overrideResults = results as? [[String: Any]] else {
-            return
-        }
-
         await context.perform {
         await context.perform {
+            guard let overrideResults = results as? [[String: Any]] else {
+                return
+            }
+
             self.isOverridesActive = overrideResults.first.map {
             self.isOverridesActive = overrideResults.first.map {
                 OverrideData(isActive: $0["enabled"] as? Bool ?? false)
                 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 direction: String?
         let change: String
         let change: String
         let date: Date
         let date: Date
-
+        let highGlucose: Decimal
+        let lowGlucose: Decimal
+        let glucoseColorScheme: String
         let detailedViewState: ContentAdditionalState?
         let detailedViewState: ContentAdditionalState?
 
 
         /// true for the first state that is set on the activity
         /// true for the first state that is set on the activity
@@ -18,8 +20,6 @@ struct LiveActivityAttributes: ActivityAttributes {
         let chart: [Decimal]
         let chart: [Decimal]
         let chartDate: [Date?]
         let chartDate: [Date?]
         let rotationDegrees: Double
         let rotationDegrees: Double
-        let highGlucose: Decimal
-        let lowGlucose: Decimal
         let cob: Decimal
         let cob: Decimal
         let iob: Decimal
         let iob: Decimal
         let unit: String
         let unit: String

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

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

+ 24 - 7
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -1,4 +1,5 @@
 import ActivityKit
 import ActivityKit
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject
@@ -29,6 +30,7 @@ import UIKit
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
+    @Injected() private var glucoseStorage: GlucoseStorage!
 
 
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
     @Published private(set) var systemEnabled: Bool
     @Published private(set) var systemEnabled: Bool
@@ -45,13 +47,20 @@ import UIKit
 
 
     let context = CoreDataStack.shared.newTaskContext()
     let context = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
         systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
         injectServices(resolver)
         injectServices(resolver)
         setupNotifications()
         setupNotifications()
-        coreDataObserver = CoreDataObserver()
+        registerSubscribers()
         registerHandler()
         registerHandler()
         monitorForLiveActivityAuthorizationChanges()
         monitorForLiveActivityAuthorizationChanges()
         setupGlucoseArray()
         setupGlucoseArray()
@@ -59,7 +68,6 @@ import UIKit
 
 
     private func setupNotifications() {
     private func setupNotifications() {
         let notificationCenter = Foundation.NotificationCenter.default
         let notificationCenter = Foundation.NotificationCenter.default
-        notificationCenter.addObserver(self, selector: #selector(handleBatchInsert), name: .didPerformBatchInsert, object: nil)
         notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
         notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
         notificationCenter
         notificationCenter
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
@@ -73,14 +81,20 @@ import UIKit
 
 
     private func registerHandler() {
     private func registerHandler() {
         // Since we are only using this info to show if an Override is active or not in the Live Activity it is enough to observe only the 'OverrideStored' Entity
         // Since we are only using this info to show if an Override is active or not in the Live Activity it is enough to observe only the 'OverrideStored' Entity
-        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.overridesDidUpdate()
             self.overridesDidUpdate()
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
-    @objc private func handleBatchInsert() {
-        setupGlucoseArray()
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupGlucoseArray()
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     @objc private func cobOrIobDidUpdate() {
     @objc private func cobOrIobDidUpdate() {
@@ -178,6 +192,9 @@ import UIKit
                         direction: nil,
                         direction: nil,
                         change: "--",
                         change: "--",
                         date: Date.now,
                         date: Date.now,
+                        highGlucose: settings.high,
+                        lowGlucose: settings.low,
+                        glucoseColorScheme: settings.glucoseColorScheme.rawValue,
                         detailedViewState: nil,
                         detailedViewState: nil,
                         isInitialState: true
                         isInitialState: true
                     ),
                     ),

+ 37 - 15
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -72,12 +72,29 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private var lastEnactedDetermination: Determination?
     private var lastEnactedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         subscribe()
         subscribe()
-        coreDataObserver = CoreDataObserver()
+
+        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()
         registerHandlers()
         setupNotification()
         setupNotification()
     }
     }
@@ -91,42 +108,47 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadStatus()
                 await self.uploadStatus()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadOverrides()
                 await self.uploadOverrides()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadOverrides()
                 await self.uploadOverrides()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadPumpHistory()
                 await self.uploadPumpHistory()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadCarbs()
                 await self.uploadCarbs()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadManualGlucose()
                 await self.uploadManualGlucose()
             }
             }
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
     func setupNotification() {
     func setupNotification() {
@@ -383,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
             var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
             modifiedFetchedEnactedDetermination?
             modifiedFetchedEnactedDetermination?
                 .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)
                 .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)

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

@@ -1,4 +1,5 @@
 import Combine
 import Combine
+import CoreData
 import Foundation
 import Foundation
 import HealthKit
 import HealthKit
 import LoopKit
 import LoopKit
@@ -9,13 +10,12 @@ protocol TidepoolManager {
     func addTidepoolService(service: Service)
     func addTidepoolService(service: Service)
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolPluginHost() -> PluginHost?
     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 {
 final class BaseTidepoolManager: TidepoolManager, Injectable {
@@ -25,6 +25,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
+    @Injected() private var apsManager: APSManager!
 
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var tidepoolService: RemoteDataService? {
     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?
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         loadTidepoolManager()
         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()
         subscribe()
     }
     }
 
 
-    /// load the Tidepool Remote Data Service if available
+    /// Loads the Tidepool service from saved state
     fileprivate func loadTidepoolManager() {
     fileprivate func loadTidepoolManager() {
         if let rawTidepoolManager = rawTidepoolManager {
         if let rawTidepoolManager = rawTidepoolManager {
             tidepoolService = tidepoolServiceFromRaw(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? {
     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? {
     func getTidepoolPluginHost() -> PluginHost? {
         self as PluginHost
         self as PluginHost
     }
     }
 
 
+    /// Adds a Tidepool service
     func addTidepoolService(service: 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? {
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
-        else {
-            return nil
-        }
+        else { return nil }
+
         if let service = serviceType.init(rawState: rawState) {
         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() {
     private func subscribe() {
-        broadcaster.register(PumpHistoryObserver.self, observer: self)
-        broadcaster.register(CarbsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
     }
     }
 
 
@@ -94,9 +142,63 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         nil
         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 }
         guard !carbs.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
 
         processQueue.async {
         processQueue.async {
@@ -108,62 +210,223 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                 tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                     switch result {
                     switch result {
                     case let .failure(error):
                     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:
                     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
             tidepoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
                 switch result {
                 switch result {
                 case let .failure(error):
                 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:
                 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 {
         processQueue.async {
             tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
             tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
@@ -171,239 +434,205 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 case let .failure(error):
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
                     debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
                 case .success:
                 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,
                     type: .tempBasal,
                     startDate: event.timestamp,
                     startDate: event.timestamp,
-                    value: 0.0,
+                    endDate: currentEndDate,
+                    value: Double(value),
                     unit: .units,
                     unit: .units,
+                    deliveredUnits: Double(value),
                     syncIdentifier: event.id,
                     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,
                     automatic: true,
                     manuallyEntered: false,
                     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 }
         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 {
         processQueue.async {
             for chunk in chunks {
             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 {
                     switch result {
                     case .success:
                     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):
                     case let .failure(error):
                         debug(.nightscout, "Error synchronizing glucose data: \(String(describing: 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 {
 extension BaseTidepoolManager: StatefulPluggableDelegate {

+ 25 - 20
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -1,4 +1,5 @@
 import AudioToolbox
 import AudioToolbox
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -56,12 +57,20 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         super.init()
         super.init()
         center.delegate = self
         center.delegate = self
         injectServices(resolver)
         injectServices(resolver)
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
@@ -70,7 +79,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             await sendGlucoseNotification()
             await sendGlucoseNotification()
         }
         }
         registerHandlers()
         registerHandlers()
-        setupGlucoseNotification()
+        registerSubscribers()
         subscribeOnLoop()
         subscribeOnLoop()
     }
     }
 
 
@@ -84,28 +93,24 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
 
     private func registerHandlers() {
     private func registerHandlers() {
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.sendGlucoseNotification()
                 await self.sendGlucoseNotification()
             }
             }
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        Task {
-            await sendGlucoseNotification()
-        }
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.sendGlucoseNotification()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     private func addAppBadge(glucose: Int?) {
     private func addAppBadge(glucose: Int?) {
@@ -227,9 +232,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             fetchLimit: 3
             fetchLimit: 3
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }

+ 60 - 49
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject
@@ -17,6 +18,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var garmin: GarminManager!
     @Injected() private var garmin: GarminManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
 
 
     private var glucoseFormatter: NumberFormatter {
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
@@ -56,7 +58,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     let context = CoreDataStack.shared.newTaskContext()
     let context = CoreDataStack.shared.newTaskContext()
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     private var lifetime = Lifetime()
     private var lifetime = Lifetime()
 
 
@@ -64,9 +67,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         self.session = session
         self.session = session
         super.init()
         super.init()
         injectServices(resolver)
         injectServices(resolver)
-        setupNotification()
-        coreDataObserver = CoreDataObserver()
         registerHandlers()
         registerHandlers()
+        registerSubscribers()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
 
 
         Task {
         Task {
             await configureState()
             await configureState()
@@ -94,42 +102,40 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         }
         }
     }
     }
 
 
-    func setupNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        Task {
-            await self.configureState()
-        }
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.configureState()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.configureState()
                 await self.configureState()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.configureState()
                 await self.configureState()
             }
             }
-        }
+        }.store(in: &subscriptions)
+
         // Observes Deletion of Glucose Objects
         // Observes Deletion of Glucose Objects
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.configureState()
                 await self.configureState()
             }
             }
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
     private func fetchlastDetermination() async -> [NSManagedObjectID] {
     private func fetchlastDetermination() async -> [NSManagedObjectID] {
@@ -142,10 +148,10 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
         return await context.perform {
         return await context.perform {
-            fetchedResults.map(\.objectID)
+            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
+            return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
@@ -160,10 +166,10 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             propertiesToFetch: ["enabled", "percentage", "objectID"]
             propertiesToFetch: ["enabled", "percentage", "objectID"]
         )
         )
 
 
-        guard let fetchedResults = results as? [[String: Any]] else { return nil }
-
         return await context.perform {
         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
         }
         }
     }
     }
 
 
@@ -178,22 +184,22 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             batchSize: 12
             batchSize: 12
         )
         )
 
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return []
-        }
-
         return await context.perform {
         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 {
         else {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination/ last Override")
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination/ last Override")
             return
             return
@@ -201,14 +207,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
 
         do {
         do {
             let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
             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(
             let recommendedInsulin = await newBolusCalc(
-                ids: glucoseValuesIDs,
-                determination: lastDetermination
+                glucoseIds: glucoseValuesIds,
+                determinationId: lastDeterminationId
             )
             )
 
 
             await MainActor.run { [weak self] in
             await MainActor.run { [weak self] in
@@ -337,9 +343,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         return description
         return description
     }
     }
 
 
-    private func newBolusCalc(ids: [NSManagedObjectID], determination: OrefDetermination?) async -> Decimal {
+    private func newBolusCalc(glucoseIds: [NSManagedObjectID], determinationId: NSManagedObjectID) async -> Decimal {
         await context.perform {
         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 {
             guard let firstGlucose = glucoseObjects.first else {
                 return 0 // If there's no glucose data, exit the block
                 return 0 // If there's no glucose data, exit the block
             }
             }
@@ -353,8 +364,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
 
             let conversion: Decimal = self.settingsManager.settings.units == .mmolL ? 0.0555 : 1
             let conversion: Decimal = self.settingsManager.settings.units == .mmolL ? 0.0555 : 1
             let isf = self.state.isf ?? 0
             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 cob = self.state.cob ?? 0
             let iob = self.state.iob ?? 0
             let iob = self.state.iob ?? 0
             let fattyMealFactor = self.settingsManager.settings.fattyMealFactor
             let fattyMealFactor = self.settingsManager.settings.fattyMealFactor

+ 1 - 1
LibreTransmitter

@@ -1 +1 @@
-Subproject commit c01eba63e94e9f6f2841a8835680c4e39c61b18d
+Subproject commit fb83de05e5505c8d114902077783c460e4ef23f0

+ 204 - 34
LiveActivity/LiveActivity.swift

@@ -17,6 +17,11 @@ enum GlucoseUnits: String, Equatable {
     static let exchangeRate: Decimal = 0.0555
     static let exchangeRate: Decimal = 0.0555
 }
 }
 
 
+enum GlucoseColorScheme: String, Equatable {
+    case staticColor
+    case dynamicColor
+}
+
 func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
 func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
     var result = Decimal()
     var result = Decimal()
     var toRound = value
     var toRound = value
@@ -60,6 +65,76 @@ extension NumberFormatter {
 }
 }
 
 
 struct LiveActivity: Widget {
 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 = {
     private let dateFormatter: DateFormatter = {
         var f = DateFormatter()
         var f = DateFormatter()
         f.dateStyle = .none
         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
         var characters = 0
 
 
         let bgText = context.state.bg
         let bgText = context.state.bg
@@ -188,16 +268,11 @@ struct LiveActivity: Widget {
         // narrow mode is for the minimal dynamic island view
         // narrow mode is for the minimal dynamic island view
         // there is not enough space to show all three arrow there
         // there is not enough space to show all three arrow there
         // and everything has to be squeezed together to some degree
         // 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 directionText: String?
-        var warnColor: Color?
         if let direction = context.state.direction {
         if let direction = context.state.direction {
             if size == .compact {
             if size == .compact {
                 directionText = String(direction[direction.startIndex ... direction.startIndex])
                 directionText = String(direction[direction.startIndex ... direction.startIndex])
-
-                if direction.count > 1 {
-                    warnColor = Color.red
-                }
             } else {
             } else {
                 directionText = direction
                 directionText = direction
             }
             }
@@ -214,17 +289,15 @@ struct LiveActivity: Widget {
 
 
         let stack = HStack(spacing: spacing) {
         let stack = HStack(spacing: spacing) {
             Text(bgText)
             Text(bgText)
+                .foregroundColor(hasStaticColorScheme ? .primary : glucoseColor)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
             if let direction = directionText {
             if let direction = directionText {
                 let text = Text(direction)
                 let text = Text(direction)
                 switch size {
                 switch size {
                 case .minimal:
                 case .minimal:
                     let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
                     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:
                 case .compact:
                     text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
                     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)
         return (stack, characters)
     }
     }
@@ -268,33 +341,61 @@ struct LiveActivity: Widget {
             let min = min(additionalState.chart.min() ?? 45, 40) - 20
             let min = min(additionalState.chart.min() ?? 45, 40) - 20
             let max = max(additionalState.chart.max() ?? 270, 300) + 50
             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
                 .asMmolL
-            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? additionalState.highGlucose : additionalState.highGlucose
+            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? context.state.highGlucose : context.state.highGlucose
                 .asMmolL
                 .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 {
             Chart {
-                RuleMark(y: .value("Low", yAxisRuleMarkMin))
-                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
                 RuleMark(y: .value("High", yAxisRuleMarkMax))
                 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]))
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
 
 
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                     let currentValue = additionalState.chart[index]
                     let currentValue = additionalState.chart[index]
                     let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
                     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 chartDate = additionalState.chartDate[index] ?? Date()
+
                     let pointMark = PointMark(
                     let pointMark = PointMark(
                         x: .value("Time", chartDate),
                         x: .value("Time", chartDate),
                         y: .value("Value", displayValue)
                         y: .value("Value", displayValue)
                     ).symbolSize(15)
                     ).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 {
             .chartYAxis {
@@ -313,9 +414,23 @@ struct LiveActivity: Widget {
     }
     }
 
 
     @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
     @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 {
         if let detailedViewState = context.state.detailedViewState {
             HStack(spacing: 12) {
             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) {
                 VStack(alignment: .leading) {
                     Spacer()
                     Spacer()
                     bgLabel(context: context, additionalState: detailedViewState)
                     bgLabel(context: context, additionalState: detailedViewState)
@@ -347,11 +462,21 @@ struct LiveActivity: Widget {
                     }
                     }
                 } else {
                 } else {
                     HStack(spacing: 3) {
                     HStack(spacing: 3) {
-                        bgAndTrend(context: context, size: .expanded).0.font(.title)
+                        bgAndTrend(
+                            context: context,
+                            size: .expanded,
+                            hasStaticColorScheme: hasStaticColorScheme,
+                            glucoseColor: glucoseColor
+                        ).0.font(.title)
                         Spacer()
                         Spacer()
                         VStack(alignment: .trailing, spacing: 5) {
                         VStack(alignment: .trailing, spacing: 5) {
                             changeLabel(context: context).font(.title3)
                             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 {
     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) {
             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) {
             DynamicIslandExpandedRegion(.trailing) {
                 changeLabel(context: context).font(.title2).padding(.trailing, 5)
                 changeLabel(context: context).font(.title2).padding(.trailing, 5)
+                    .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
             }
             }
             DynamicIslandExpandedRegion(.bottom) {
             DynamicIslandExpandedRegion(.bottom) {
                 if context.state.isInitialState {
                 if context.state.isInitialState {
@@ -396,11 +542,17 @@ struct LiveActivity: Widget {
                 }
                 }
             }
             }
         } compactLeading: {
         } compactLeading: {
-            bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
+            bgAndTrend(context: context, size: .compact, hasStaticColorScheme: hasStaticColorScheme, glucoseColor: glucoseColor).0
+                .padding(.leading, 4)
         } compactTrailing: {
         } compactTrailing: {
-            changeLabel(context: context).padding(.trailing, 4)
+            changeLabel(context: context).padding(.trailing, 4).foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
         } minimal: {
         } 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)
             let label = _label.padding(.leading, 7).padding(.trailing, 3)
 
 
             if characterCount < 4 {
             if characterCount < 4 {
@@ -412,7 +564,7 @@ struct LiveActivity: Widget {
             }
             }
         }
         }
         .widgetURL(URL(string: "Trio://"))
         .widgetURL(URL(string: "Trio://"))
-        .keylineTint(Color.purple)
+        .keylineTint(hasStaticColorScheme ? Color.purple : glucoseColor)
         .contentMargins(.horizontal, 0, for: .minimal)
         .contentMargins(.horizontal, 0, for: .minimal)
         .contentMargins(.trailing, 0, for: .compactLeading)
         .contentMargins(.trailing, 0, for: .compactLeading)
         .contentMargins(.leading, 0, for: .compactTrailing)
         .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
     // 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 {
     static var testWide: LiveActivityAttributes.ContentState {
         LiveActivityAttributes.ContentState(
         LiveActivityAttributes.ContentState(
-            bg: "00.0",
+            bg: 00.0.description,
             direction: "→",
             direction: "→",
             change: "+0.0",
             change: "+0.0",
             date: Date(),
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             detailedViewState: nil,
             isInitialState: false
             isInitialState: false
         )
         )
@@ -450,6 +605,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑↑",
             direction: "↑↑",
             change: "+0.0",
             change: "+0.0",
             date: Date(),
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             detailedViewState: nil,
             isInitialState: false
             isInitialState: false
         )
         )
@@ -461,6 +619,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑↑↑",
             direction: "↑↑↑",
             change: "+0.0",
             change: "+0.0",
             date: Date(),
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             detailedViewState: nil,
             isInitialState: false
             isInitialState: false
         )
         )
@@ -473,6 +634,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑",
             direction: "↑",
             change: "+0",
             change: "+0",
             date: Date(),
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             detailedViewState: nil,
             isInitialState: false
             isInitialState: false
         )
         )
@@ -484,6 +648,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↗︎",
             direction: "↗︎",
             change: "+00",
             change: "+00",
             date: Date(),
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             detailedViewState: nil,
             isInitialState: false
             isInitialState: false
         )
         )
@@ -495,6 +662,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: nil,
             direction: nil,
             change: "--",
             change: "--",
             date: Date().addingTimeInterval(-60 * 60),
             date: Date().addingTimeInterval(-60 * 60),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             detailedViewState: nil,
             isInitialState: true
             isInitialState: true
         )
         )

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

@@ -13,6 +13,8 @@ public extension CarbEntryStored {
     @NSManaged var id: UUID?
     @NSManaged var id: UUID?
     @NSManaged var isFPU: Bool
     @NSManaged var isFPU: Bool
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var note: String?
     @NSManaged var protein: Double
     @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 id: UUID?
     @NSManaged var isManual: Bool
     @NSManaged var isManual: Bool
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
 }
 }
 
 
 extension GlucoseStored: Identifiable {}
 extension GlucoseStored: Identifiable {}

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

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

+ 15 - 36
Model/CoreDataObserver.swift

@@ -1,46 +1,25 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 
 
-class CoreDataObserver {
-    private var entityUpdateHandlers: [String: () -> Void] = [:] // Dictionary to store pairs of entities and handlers
+func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObject>, Never> {
+    Foundation.NotificationCenter.default
+        .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
+        .map { notification in
+            guard let userInfo = notification.userInfo else { return Set<NSManagedObject>() }
 
 
-    init() {
-        setupNotification()
-    }
-
-    func registerHandler(for entityName: String, handler: @escaping () -> Void) {
-        entityUpdateHandlers[entityName] = handler
-    }
-
-    private func setupNotification() {
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(contextDidSave(_:)),
-            name: NSNotification.Name.NSManagedObjectContextDidSave,
-            object: nil
-        )
-    }
+            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
 
 
-    @objc private func contextDidSave(_ notification: Notification) {
-        guard let userInfo = notification.userInfo else { return }
-
-        Task {
-            await processUpdates(userInfo: userInfo)
+            return objects
         }
         }
-    }
-
-    private func processUpdates(userInfo: [AnyHashable: Any]) async {
-        var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-        objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-        objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+}
 
 
-        for (entityName, handler) in entityUpdateHandlers {
-            let entityUpdates = objects.filter { $0.entity.name == entityName }
-            DispatchQueue.global(qos: .background).async {
-                if entityUpdates.isNotEmpty {
-                    handler()
-                }
-            }
+extension Publisher where Output == Set<NSManagedObject> {
+    func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
+        filter { objects in
+            objects.contains(where: { $0.entity.name == name })
         }
         }
     }
     }
 }
 }

+ 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 {
     static var fpusNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(
         return NSPredicate(
@@ -71,7 +89,7 @@ extension CarbEntryStored: Encodable {
         try container.encode(formattedDate, forKey: .created_at)
         try container.encode(formattedDate, forKey: .created_at)
 
 
         // TODO: handle this conditionally; pass in the enteredBy string (manual entry or via NS or Apple Health)
         // 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(carbs, forKey: .carbs)
         try container.encode(fat, forKey: .fat)
         try container.encode(fat, forKey: .fat)

+ 0 - 4
Model/Helper/CustomNotification.swift

@@ -1,10 +1,6 @@
 import Foundation
 import Foundation
 
 
 extension Notification.Name {
 extension Notification.Name {
-    static let didPerformBatchInsert = Notification.Name("didPerformBatchInsert")
-    static let didPerformBatchUpdate = Notification.Name("didPerformBatchUpdate")
-    static let didPerformBatchDelete = Notification.Name("didPerformBatchDelete")
-    static let didUpdateDetermination = Notification.Name("didUpdateDetermination")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
     static let didUpdateCobIob = Notification.Name("didUpdateCobIob")
     static let didUpdateCobIob = Notification.Name("didUpdateCobIob")
 }
 }

+ 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)
         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 {
     static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(
         return NSPredicate(
@@ -79,6 +89,26 @@ extension NSPredicate {
             true as NSNumber
             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 {
 extension GlucoseStored: Encodable {

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

@@ -82,6 +82,16 @@ extension NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
         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
 // 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="fpuID" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" 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="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="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="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
@@ -44,9 +46,11 @@
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="isManual" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <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="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
-            <fetchIndexElement property="date" type="Binary" order="descending"/>
+            <fetchIndexElement property="date" type="Binary" order="ascending"/>
         </fetchIndex>
         </fetchIndex>
         <fetchIndex name="byIsManual">
         <fetchIndex name="byIsManual">
             <fetchIndexElement property="isManual" type="Binary" order="ascending"/>
             <fetchIndexElement property="isManual" type="Binary" order="ascending"/>
@@ -54,6 +58,9 @@
         <fetchIndex name="byIsUploadedToNS">
         <fetchIndex name="byIsUploadedToNS">
             <fetchIndexElement property="isUploadedToNS" type="Binary" order="ascending"/>
             <fetchIndexElement property="isUploadedToNS" type="Binary" order="ascending"/>
         </fetchIndex>
         </fetchIndex>
+        <fetchIndex name="byIsUploadedToHealth">
+            <fetchIndexElement property="isUploadedToHealth" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     </entity>
     <entity name="LoopStatRecord" representedClassName="LoopStatRecord" syncable="YES">
     <entity name="LoopStatRecord" representedClassName="LoopStatRecord" syncable="YES">
         <attribute name="duration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <attribute name="duration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
@@ -161,7 +168,9 @@
     </entity>
     </entity>
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
         <attribute name="id" optional="YES" attributeType="String"/>
         <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="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="note" optional="YES" attributeType="String"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="type" optional="YES" attributeType="String"/>
         <attribute name="type" optional="YES" attributeType="String"/>

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit eacf06f7873e73d6cb8ccd0556b35f734b90df40
+Subproject commit e39834584548821adf442f13abed0d5cfd237a72

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 03d3a1db5a4da9b218a60254fa1b0ea72ee808ed
+Subproject commit 849dc7abc821728dae7e064176a409e6ceb0dadd

+ 1 - 1
TidepoolService

@@ -1 +1 @@
-Subproject commit a2ccad72a55600c28549ab86ab1964c0d6558868
+Subproject commit b28625628e181b96f0db7ec3739d920a3c92465b

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

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