Procházet zdrojové kódy

merged from upstream

kskandis před 1 rokem
rodič
revize
05c96d4757
100 změnil soubory, kde provedl 3624 přidání a 1513 odebrání
  1. 1 4
      .github/workflows/add_identifiers.yml
  2. 2 2
      .github/workflows/build_trio.yml
  3. 1 4
      .github/workflows/create_certs.yml
  4. 3 3
      .github/workflows/validate_secrets.yml
  5. 1 1
      CODE_OF_CONDUCT.md
  6. 1 1
      Config.xcconfig
  7. 153 17
      FreeAPS.xcodeproj/project.pbxproj
  8. 16 0
      FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  9. 2 0
      FreeAPS/Resources/FreeAPS.entitlements
  10. 2 0
      FreeAPS/Resources/Info.plist
  11. 1 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  12. 6 4
      FreeAPS/Sources/APS/APSManager.swift
  13. 24 35
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  14. 1 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  15. 71 6
      FreeAPS/Sources/APS/OpenAPS/JavaScriptWorker.swift
  16. 14 43
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  17. 88 12
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  18. 36 4
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  19. 165 5
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  20. 38 10
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  21. 112 3
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  22. 51 1
      FreeAPS/Sources/Application/AppDelegate.swift
  23. 6 1
      FreeAPS/Sources/Helpers/Color+Extensions.swift
  24. 66 0
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  25. 4 0
      FreeAPS/Sources/Logger/Logger.swift
  26. 3 2
      FreeAPS/Sources/Models/BloodGlucose.swift
  27. 1 1
      FreeAPS/Sources/Models/CarbsEntry.swift
  28. 11 5
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  29. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  30. 22 0
      FreeAPS/Sources/Models/GlucoseColorScheme.swift
  31. 12 0
      FreeAPS/Sources/Models/NightscoutStatus.swift
  32. 1 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  33. 141 0
      FreeAPS/Sources/Models/PushMessage.swift
  34. 22 21
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  35. 1 1
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  36. 8 7
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  37. 1 1
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  38. 9 8
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  39. 1 1
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  40. 126 121
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  41. 1 0
      FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift
  42. 110 99
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  43. 72 58
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  44. 1 1
      FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift
  45. 1 1
      FreeAPS/Sources/Modules/Bolus/View/PopupView.swift
  46. 10 10
      FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  47. 1 1
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift
  48. 2 2
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  49. 7 2
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  50. 36 34
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  51. 139 63
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  52. 3 1
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  53. 13 12
      FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift
  54. 1 1
      FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  55. 18 18
      FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  56. 2 2
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  57. 16 14
      FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift
  58. 6 0
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  59. 32 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  60. 60 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  61. 101 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  62. 58 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  63. 111 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  64. 37 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  65. 98 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  66. 81 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  67. 158 562
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  68. 4 4
      FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift
  69. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/CobChart.swift
  70. 30 4
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  71. 15 3
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  72. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/IobChart.swift
  73. 47 120
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  74. 35 45
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  75. 6 6
      FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift
  76. 7 7
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  77. 38 33
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  78. 9 8
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  79. 1 1
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  80. 0 3
      FreeAPS/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift
  81. 12 0
      FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift
  82. 388 0
      FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  83. 5 4
      FreeAPS/Sources/Modules/ManualTempBasal/ManualTempBasalStateModel.swift
  84. 1 1
      FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift
  85. 3 6
      FreeAPS/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  86. 23 2
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  87. 14 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  88. 63 54
      FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift
  89. 6 3
      FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift
  90. 1 1
      FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift
  91. 36 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift
  92. 108 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Bolus.swift
  93. 12 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Helpers.swift
  94. 82 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Meal.swift
  95. 100 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift
  96. 49 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+TempTarget.swift
  97. 113 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift
  98. 6 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigDataFlow.swift
  99. 4 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigProvider.swift
  100. 0 0
      FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigStateModel.swift

+ 1 - 4
.github/workflows/add_identifiers.yml

@@ -12,11 +12,8 @@ jobs:
   identifiers:
   identifiers:
     name: Add Identifiers
     name: Add Identifiers
     needs: validate
     needs: validate
-    runs-on: macos-14
+    runs-on: macos-15
     steps:
     steps:
-      # Uncomment to manually select latest Xcode if needed
-      #- name: Select Latest Xcode
-      #  run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer"
 
 
       # Checks-out the repo
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo

+ 2 - 2
.github/workflows/build_trio.yml

@@ -186,7 +186,7 @@ jobs:
   build:
   build:
     name: Build
     name: Build
     needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
     needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
-    runs-on: macos-14
+    runs-on: macos-15
     permissions:
     permissions:
       contents: write
       contents: write
     if:
     if:
@@ -198,7 +198,7 @@ jobs:
       )
       )
     steps:
     steps:
       - name: Select Xcode version
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_15.4.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_16.0.app/Contents/Developer"
       
       
       - name: Checkout Repo for syncing
       - name: Checkout Repo for syncing
         if: |
         if: |

+ 1 - 4
.github/workflows/create_certs.yml

@@ -12,11 +12,8 @@ jobs:
   certificates:
   certificates:
     name: Create Certificates
     name: Create Certificates
     needs: validate
     needs: validate
-    runs-on: macos-14
+    runs-on: macos-15
     steps:
     steps:
-      # Uncomment to manually select latest Xcode if needed
-      #- name: Select Latest Xcode
-      #  run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer"
 
 
       # Checks-out the repo
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo

+ 3 - 3
.github/workflows/validate_secrets.yml

@@ -5,7 +5,7 @@ on: [workflow_call, workflow_dispatch]
 jobs:
 jobs:
   validate-access-token:
   validate-access-token:
     name: Access
     name: Access
-    runs-on: macos-14
+    runs-on: macos-15
     env:
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -74,7 +74,7 @@ jobs:
   validate-match-secrets:
   validate-match-secrets:
     name: Match-Secrets
     name: Match-Secrets
     needs: validate-access-token
     needs: validate-access-token
-    runs-on: macos-14
+    runs-on: macos-15
     env:
     env:
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
     steps:
     steps:
@@ -112,7 +112,7 @@ jobs:
   validate-fastlane-secrets:
   validate-fastlane-secrets:
     name: Fastlane
     name: Fastlane
     needs: [validate-access-token, validate-match-secrets]
     needs: [validate-access-token, validate-match-secrets]
-    runs-on: macos-14
+    runs-on: macos-15
     env:
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -59,7 +59,7 @@ representative at an online or offline event.
 ## Enforcement
 ## Enforcement
 
 
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the Discord server admins. Please join our [Discord server](https://discord.gg/dbe5Twav8D) to contact
+reported to the Discord server admins. Please join our [Discord server](http://discord.diy-trio.org) to contact
 them directly for any enforcement issues. All complaints will be reviewed and
 them directly for any enforcement issues. All complaints will be reviewed and
 investigated promptly and fairly.
 investigated promptly and fairly.
 
 

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = Trio
 APP_DISPLAY_NAME = Trio
-APP_VERSION = 0.2.0
+APP_VERSION = 0.3.0
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##
 DEVELOPER_TEAM = ##TEAM_ID##

+ 153 - 17
FreeAPS.xcodeproj/project.pbxproj

@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	archiveVersion = 1;
 	classes = {
 	classes = {
 	};
 	};
-	objectVersion = 54;
+	objectVersion = 70;
 	objects = {
 	objects = {
 
 
 /* Begin PBXBuildFile section */
 /* Begin PBXBuildFile section */
@@ -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 */; };
@@ -314,12 +322,13 @@
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
+		BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		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 */; };
@@ -434,6 +443,12 @@
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
+		DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */; };
+		DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */; };
+		DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */; };
+		DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */; };
+		DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */; };
+		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
@@ -442,6 +457,15 @@
 		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 */; };
 		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 */; };
+		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
+		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
+		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
+		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
+		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
+		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
+		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.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 */; };
@@ -909,6 +933,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>"; };
@@ -968,12 +1000,13 @@
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
+		BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetConfiguration.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		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>"; };
@@ -1090,6 +1123,12 @@
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
+		DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Bolus.swift"; sourceTree = "<group>"; };
+		DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Meal.swift"; sourceTree = "<group>"; };
+		DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+TempTarget.swift"; sourceTree = "<group>"; };
+		DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Override.swift"; sourceTree = "<group>"; };
+		DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+APNS.swift"; sourceTree = "<group>"; };
+		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
@@ -1098,6 +1137,15 @@
 		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>"; };
 		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>"; };
+		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
+		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
+		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
+		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
+		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
+		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
+		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.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>"; };
@@ -1169,6 +1217,10 @@
 		FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
 		FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 /* End PBXFileReference section */
 
 
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+		DDCEBF412CC1B42500DF4C36 /* Views */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Views; sourceTree = "<group>"; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
 /* Begin PBXFrameworksBuildPhase section */
 /* Begin PBXFrameworksBuildPhase section */
 		388E595525AD948C0019842D /* Frameworks */ = {
 		388E595525AD948C0019842D /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			isa = PBXFrameworksBuildPhase;
@@ -1451,6 +1503,8 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
+				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
@@ -1556,6 +1610,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;
@@ -1897,6 +1952,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
@@ -1941,6 +1997,7 @@
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
 				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1948,6 +2005,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 */,
@@ -2250,6 +2308,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 = (
@@ -2287,10 +2360,12 @@
 		6B1A8D1C2B14D91600E76752 /* LiveActivity */ = {
 		6B1A8D1C2B14D91600E76752 /* LiveActivity */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDCEBF412CC1B42500DF4C36 /* Views */,
 				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
 				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
 				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
 				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
 				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
 				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
 				6B1A8D252B14D91700E76752 /* Info.plist */,
 				6B1A8D252B14D91700E76752 /* Info.plist */,
+				DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */,
 			);
 			);
 			path = LiveActivity;
 			path = LiveActivity;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2352,7 +2427,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 */,
 			);
 			);
@@ -2660,6 +2735,39 @@
 			path = ProfileImport;
 			path = ProfileImport;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
+			isa = PBXGroup;
+			children = (
+				DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */,
+				DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */,
+				DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */,
+				DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */,
+				DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */,
+				DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */,
+				DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */,
+			);
+			path = RemoteControl;
+			sourceTree = "<group>";
+		};
+		DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */ = {
+			isa = PBXGroup;
+			children = (
+				DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */,
+				DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */,
+				DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */,
+				DD9ECB6C2CA99FAE00AA7C45 /* View */,
+			);
+			path = RemoteControlConfig;
+			sourceTree = "<group>";
+		};
+		DD9ECB6C2CA99FAE00AA7C45 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		DDD163032C4C67B400CD525A /* OverrideConfig */ = {
 		DDD163032C4C67B400CD525A /* OverrideConfig */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -2735,6 +2843,7 @@
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
 				DDF847E32C5C288F0049BB3B /* LiveActivitySettingsRootView.swift */,
 				DDF847E32C5C288F0049BB3B /* LiveActivitySettingsRootView.swift */,
+				BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */,
 			);
 			);
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2932,6 +3041,9 @@
 			);
 			);
 			dependencies = (
 			dependencies = (
 			);
 			);
+			fileSystemSynchronizedGroups = (
+				DDCEBF412CC1B42500DF4C36 /* Views */,
+			);
 			name = LiveActivityExtension;
 			name = LiveActivityExtension;
 			productName = LiveActivityExtension;
 			productName = LiveActivityExtension;
 			productReference = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */;
 			productReference = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */;
@@ -3152,12 +3264,14 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
+				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				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 */,
@@ -3183,7 +3297,9 @@
 				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 */,
+				DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
@@ -3236,11 +3352,14 @@
 				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 */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
+				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
+				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
@@ -3270,6 +3389,7 @@
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
+				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
@@ -3285,6 +3405,7 @@
 				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 */,
@@ -3295,6 +3416,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 */,
@@ -3316,6 +3438,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
+				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
 				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
@@ -3339,6 +3462,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 */,
@@ -3375,17 +3499,20 @@
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
+				DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				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 */,
@@ -3402,6 +3529,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 */,
@@ -3422,6 +3550,7 @@
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
+				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
@@ -3449,6 +3578,8 @@
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
+				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
@@ -3492,9 +3623,11 @@
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
+				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
+				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
@@ -3512,6 +3645,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 */,
@@ -3582,6 +3716,7 @@
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
 				DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */,
 				DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */,
 				DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */,
 				DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */,
+				DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */,
 				DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */,
 				DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */,
 				CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.swift in Sources */,
 				CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.swift in Sources */,
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,
@@ -3632,6 +3767,7 @@
 			files = (
 			files = (
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
+				DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
 			);
 			);
@@ -3781,7 +3917,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;
@@ -3841,7 +3977,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;
@@ -3870,7 +4006,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",
@@ -3911,7 +4047,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",
@@ -3968,7 +4104,7 @@
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_VERSION = 5.0;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			};
 			name = Debug;
 			name = Debug;
 		};
 		};
@@ -4005,7 +4141,7 @@
 				SWIFT_OBJC_BRIDGING_HEADER = "Model/Classes+Properties/FreeAPSWatch-Bridging-Header.h";
 				SWIFT_OBJC_BRIDGING_HEADER = "Model/Classes+Properties/FreeAPSWatch-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			};
 			name = Release;
 			name = Release;
 		};
 		};
@@ -4045,7 +4181,7 @@
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_VERSION = 5.0;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			};
 			name = Debug;
 			name = Debug;
 		};
 		};
@@ -4085,7 +4221,7 @@
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_VERSION = 5.0;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			};
 			name = Release;
 			name = Release;
 		};
 		};
@@ -4096,7 +4232,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",
@@ -4117,7 +4253,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",
@@ -4147,7 +4283,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",
@@ -4181,7 +4317,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"

+ 2 - 0
FreeAPS/Resources/FreeAPS.entitlements

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
+	<key>aps-environment</key>
+	<string>development</string>
 	<key>com.apple.developer.healthkit</key>
 	<key>com.apple.developer.healthkit</key>
 	<true/>
 	<true/>
 	<key>com.apple.developer.healthkit.access</key>
 	<key>com.apple.developer.healthkit.access</key>

+ 2 - 0
FreeAPS/Resources/Info.plist

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
+	<key>TeamID</key>
+	<string>$(DEVELOPER_TEAM)</string>
 	<key>AppGroupID</key>
 	<key>AppGroupID</key>
 	<string>$(APP_GROUP_ID)</string>
 	<string>$(APP_GROUP_ID)</string>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>

+ 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...

+ 24 - 35
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -1,5 +1,6 @@
 import Combine
 import Combine
 import Foundation
 import Foundation
+import HealthKit
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
 import SwiftDate
 import SwiftDate
@@ -28,6 +29,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!
@@ -100,6 +102,14 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
         settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
     }
     }
 
 
+    private func updateManagerUnits(_ manager: CGMManagerUI?) {
+        let units = settingsManager.settings.units
+        let managerName = cgmManager.map { "\(type(of: $0))" } ?? "nil"
+        let loopkitUnits: HKUnit = units == .mgdL ? .milligramsPerDeciliter : .millimolesPerLiter
+        print("manager: \(managerName) is changing units to: \(loopkitUnits.description) ")
+        manager?.unitDidChange(to: loopkitUnits)
+    }
+
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
         // if changed, remove all calibrations
         // if changed, remove all calibrations
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
@@ -122,6 +132,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             removeCalibrations()
             removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
+            updateManagerUnits(cgmManager)
+
         } else {
         } else {
             saveConfigManager()
             saveConfigManager()
         }
         }
@@ -151,7 +163,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         else {
         else {
             return nil
             return nil
         }
         }
-
         return Manager.init(rawState: rawState)
         return Manager.init(rawState: rawState)
     }
     }
 
 
@@ -165,18 +176,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 +219,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 +234,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 +242,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 +273,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 +294,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)

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

@@ -87,6 +87,7 @@ extension OpenAPS {
         static let uploadedPreferences = "upload/uploaded-preferences.json"
         static let uploadedPreferences = "upload/uploaded-preferences.json"
         static let uploadedSettings = "upload/uploaded-settings.json"
         static let uploadedSettings = "upload/uploaded-settings.json"
         static let uploadedManualGlucose = "upload/uploaded-manual-readings.json"
         static let uploadedManualGlucose = "upload/uploaded-manual-readings.json"
+        static let uploadedNotes = "upload/uploaded-notes.json"
     }
     }
 
 
     enum FreeAPS {
     enum FreeAPS {

+ 71 - 6
FreeAPS/Sources/APS/OpenAPS/JavaScriptWorker.swift

@@ -1,11 +1,36 @@
 import Foundation
 import Foundation
 import JavaScriptCore
 import JavaScriptCore
 
 
+private let contextLock = NSRecursiveLock()
+
+extension String {
+    var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }
+    var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
+    var camelCased: String {
+        guard !isEmpty else { return "" }
+        let parts = components(separatedBy: .alphanumerics.inverted)
+        let first = parts.first!.lowercasingFirst
+        let rest = parts.dropFirst().map(\.uppercasingFirst)
+        return ([first] + rest).joined()
+    }
+
+    var pascalCased: String {
+        guard !isEmpty else { return "" }
+        let parts = components(separatedBy: .alphanumerics.inverted)
+        let first = parts.first!.uppercasingFirst
+        let rest = parts.dropFirst().map(\.uppercasingFirst)
+        return ([first] + rest).joined()
+    }
+}
+
 final class JavaScriptWorker {
 final class JavaScriptWorker {
     private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker", attributes: .concurrent)
     private let processQueue = DispatchQueue(label: "DispatchQueue.JavaScriptWorker", attributes: .concurrent)
     private let virtualMachine: JSVirtualMachine
     private let virtualMachine: JSVirtualMachine
     private var contextPool: [JSContext] = []
     private var contextPool: [JSContext] = []
     private let contextPoolLock = NSLock()
     private let contextPoolLock = NSLock()
+    @SyncAccess(lock: contextLock) private var commonContext: JSContext? = nil
+    private var consoleLogs: [String] = []
+    private var logContext: String = ""
 
 
     init(poolSize: Int = 5) {
     init(poolSize: Int = 5) {
         virtualMachine = JSVirtualMachine()!
         virtualMachine = JSVirtualMachine()!
@@ -23,7 +48,10 @@ final class JavaScriptWorker {
             }
             }
         }
         }
         let consoleLog: @convention(block) (String) -> Void = { message in
         let consoleLog: @convention(block) (String) -> Void = { message in
-            debug(.openAPS, "JavaScript log: \(message)")
+            let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
+            if !trimmedMessage.isEmpty {
+                self.consoleLogs.append("\(trimmedMessage)")
+            }
         }
         }
         context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
         context.setObject(consoleLog, forKeyedSubscript: "_consoleLog" as NSString)
         return context
         return context
@@ -42,8 +70,37 @@ final class JavaScriptWorker {
         contextPoolLock.unlock()
         contextPoolLock.unlock()
     }
     }
 
 
+    // New method to flush aggregated logs
+    private func outputLogs() {
+        var outputLogs = consoleLogs.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
+        consoleLogs.removeAll()
+
+        if outputLogs.isEmpty { return }
+
+        if logContext == "autosens.js" {
+            outputLogs = outputLogs.split(separator: "\n").map { logLine in
+                logLine.replacingOccurrences(
+                    of: "^[-+=x!]|u\\(|\\)|\\d{1,2}h$",
+                    with: "",
+                    options: .regularExpression
+                )
+            }.joined(separator: "\n")
+        }
+
+        if !outputLogs.isEmpty {
+            outputLogs.split(separator: "\n").forEach { logLine in
+                if !"\(logLine)".trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+                    debug(.openAPS, "\(logContext): \(logLine)")
+                }
+            }
+        }
+    }
+
     @discardableResult func evaluate(script: Script) -> JSValue! {
     @discardableResult func evaluate(script: Script) -> JSValue! {
-        evaluate(string: script.body)
+        logContext = URL(fileURLWithPath: script.name).lastPathComponent
+        let result = evaluate(string: script.body)
+        outputLogs()
+        return result
     }
     }
 
 
     private func evaluate(string: String) -> JSValue! {
     private func evaluate(string: String) -> JSValue! {
@@ -63,15 +120,23 @@ final class JavaScriptWorker {
 
 
     func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
     func inCommonContext<Value>(execute: (JavaScriptWorker) -> Value) -> Value {
         let context = getContext()
         let context = getContext()
-        defer { returnContext(context) }
+        defer {
+            returnContext(context)
+            outputLogs()
+        }
         return execute(self)
         return execute(self)
     }
     }
 
 
     func evaluateBatch(scripts: [Script]) {
     func evaluateBatch(scripts: [Script]) {
-        let ctx = getContext()
-        defer { returnContext(ctx) } // Ensure the context is returned to the pool
+        let context = getContext()
+        defer {
+            // Ensure the context is returned to the pool
+            returnContext(context)
+        }
         scripts.forEach { script in
         scripts.forEach { script in
-            ctx.evaluateScript(script.body)
+            logContext = URL(fileURLWithPath: script.name).lastPathComponent
+            context.evaluateScript(script.body)
+            outputLogs()
         }
         }
     }
     }
 }
 }

+ 14 - 43
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -86,35 +86,6 @@ final class OpenAPS {
 
 
         // First save the current Determination to Core Data
         // First save the current Determination to Core Data
         await attemptToSaveContext()
         await attemptToSaveContext()
-
-        // After that check for changes in iob and cob and if there are any post a custom Notification
-        /// this is currently used to update Live Activity so that it stays up to date and not one loop cycle behind
-        await checkForCobIobUpdate(determination)
-    }
-
-    func checkForCobIobUpdate(_ determination: Determination) async {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: context,
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination,
-            key: "deliverAt",
-            ascending: false,
-            fetchLimit: 2
-        )
-
-        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))
-
-            if iobChanged || cobChanged {
-                Foundation.NotificationCenter.default.post(name: .didUpdateCobIob, object: nil)
-            }
-        }
     }
     }
 
 
     func attemptToSaveContext() async {
     func attemptToSaveContext() async {
@@ -140,11 +111,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 +130,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 +180,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)
         }
         }
     }
     }
@@ -1000,11 +971,11 @@ final class OpenAPS {
 
 
     private func middlewareScript(name: String) -> Script? {
     private func middlewareScript(name: String) -> Script? {
         if let body = storage.retrieveRaw(name) {
         if let body = storage.retrieveRaw(name) {
-            return Script(name: "Middleware", body: body)
+            return Script(name: name, body: body)
         }
         }
 
 
         if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
         if let url = Foundation.Bundle.main.url(forResource: "javascript/\(name)", withExtension: "") {
-            return Script(name: "Middleware", body: try! String(contentsOf: url))
+            return Script(name: name, body: try! String(contentsOf: url))
         }
         }
 
 
         return nil
         return nil

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

@@ -17,6 +17,8 @@ protocol CarbsStorage {
     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 {
@@ -44,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] {
@@ -63,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
@@ -115,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,
@@ -144,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
 
 
@@ -161,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)
@@ -202,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 }
@@ -213,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(),
@@ -228,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 {
@@ -337,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,
@@ -376,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,
@@ -402,4 +416,66 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             }
             }
         }
         }
     }
     }
+
+    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToHealth,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: Decimal(result.fat),
+                    protein: Decimal(result.protein),
+                    note: result.note,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: result.isFPU,
+                    fpuID: result.fpuID?.uuidString
+                )
+            }
+        }
+    }
+
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: nil,
+                    protein: nil,
+                    note: result.note,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: nil,
+                    fpuID: nil
+                )
+            }
+        }
+    }
 }
 }

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

@@ -7,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?
 }
 }
 
 
@@ -28,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)
         }
         }
     }
     }
 
 
@@ -39,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 []
                 }
                 }
@@ -73,6 +77,34 @@ 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]) {
+        return await context.perform {
+            var forecast: Forecast?
+            var forecastValues: [ForecastValue] = []
+
+            do {
+                // 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

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

@@ -2,6 +2,7 @@ import AVFAudio
 import Combine
 import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import LoopKit
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
@@ -17,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 {
@@ -88,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
                     }
                     }
                 )
                 )
@@ -241,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,
@@ -252,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,
@@ -272,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,
@@ -295,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",
@@ -326,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 {

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

@@ -12,6 +12,7 @@ protocol OverrideStorage {
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
+    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride]
 }
 }
 
 
 final class BaseOverrideStorage: OverrideStorage, Injectable {
 final class BaseOverrideStorage: OverrideStorage, Injectable {
@@ -45,9 +46,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 +63,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 +80,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)
         }
         }
     }
     }
@@ -220,9 +221,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(
@@ -250,9 +251,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
@@ -267,4 +268,31 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             }
             }
         }
         }
     }
     }
+
+    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.allOverridePresets,
+            key: "orderPosition",
+            ascending: true
+        )
+
+        return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
+            return fetchedResults.map { overrideStored in
+                let duration = overrideStored.duration as? Decimal != 0 ? overrideStored.duration as? Decimal : nil
+                let percentage = overrideStored.percentage != 0 ? overrideStored.percentage : nil
+                let target = (overrideStored.target as? Decimal) != 0 ? overrideStored.target as? Decimal : nil
+
+                return NightscoutPresetOverride(
+                    name: overrideStored.name ?? "",
+                    duration: duration,
+                    percentage: percentage,
+                    target: target
+                )
+            }
+        }
+    }
 }
 }

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

@@ -15,6 +15,8 @@ protocol PumpHistoryStorage {
     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)
 }
 }
 
 
@@ -80,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)")
                                     }
                                     }
@@ -93,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
@@ -122,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
@@ -140,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 {
@@ -152,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 {
@@ -164,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 {
@@ -176,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 {
@@ -188,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:
@@ -217,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)
@@ -274,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)
@@ -423,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 }
+        }
+    }
 }
 }

+ 51 - 1
FreeAPS/Sources/Application/AppDelegate.swift

@@ -1,4 +1,54 @@
 import SwiftUI
 import SwiftUI
 import UIKit
 import UIKit
+import UserNotifications
 
 
-class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {}
+class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNotificationCenterDelegate {
+    func application(
+        _ application: UIApplication,
+        didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
+    ) -> Bool {
+        UNUserNotificationCenter.current().delegate = self
+        application.registerForRemoteNotifications()
+        return true
+    }
+
+    func application(
+        _: UIApplication,
+        didReceiveRemoteNotification userInfo: [AnyHashable: Any],
+        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
+    ) {
+        debug(.remoteControl, "Received notification")
+
+        do {
+            let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
+            let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
+
+            Task {
+                await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                completionHandler(.newData)
+            }
+        } catch {
+            debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
+            completionHandler(.failed)
+        }
+    }
+
+    func application(
+        _: UIApplication,
+        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
+    ) {
+        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
+        let token = tokenParts.joined()
+
+        Task {
+            await TrioRemoteControl.shared.handleAPNSChanges(deviceToken: token)
+        }
+    }
+
+    func application(
+        _: UIApplication,
+        didFailToRegisterForRemoteNotificationsWithError error: Error
+    ) {
+        debug(.remoteControl, "Failed to register for remote notifications: \(error.localizedDescription)")
+    }
+}

+ 6 - 1
FreeAPS/Sources/Helpers/Color+Extensions.swift

@@ -8,7 +8,12 @@ extension Color {
 
 
     static let glucose = Color("glucose")
     static let glucose = Color("glucose")
 
 
-    static let insulin = Color("Insulin")
+    static let insulin =
+        // workaround for 'No color named 'Insulin' found in asset catalog' error which is most likely a bug
+        Color(
+            UIColor(named: "Insulin") ??
+                UIColor(red: 0.118, green: 0.588, blue: 0.988, alpha: 1.0) // these are RGB of our insulin color
+        )
 
 
     // The loopAccent color is intended to be use as the app accent color.
     // The loopAccent color is intended to be use as the app accent color.
     public static let loopAccent = Color("accent")
     public static let loopAccent = Color("accent")

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

@@ -0,0 +1,66 @@
+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
+) -> Color {
+    // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
+    if glucoseColorScheme == .dynamicColor {
+        return calculateHueBasedGlucoseColor(
+            glucoseValue: glucoseValue,
+            highGlucose: highGlucoseColorValue,
+            lowGlucose: lowGlucoseColorValue,
+            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
+}

+ 4 - 0
FreeAPS/Sources/Logger/Logger.swift

@@ -112,6 +112,7 @@ final class Logger {
     static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter)
     static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter)
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
+    static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
 
 
     enum Category: String {
     enum Category: String {
         case `default`
         case `default`
@@ -121,6 +122,7 @@ final class Logger {
         case deviceManager
         case deviceManager
         case apsManager
         case apsManager
         case nightscout
         case nightscout
+        case remoteControl
 
 
         var name: String {
         var name: String {
             rawValue.capitalizingFirstLetter()
             rawValue.capitalizingFirstLetter()
@@ -135,6 +137,7 @@ final class Logger {
             case .deviceManager: return .deviceManager
             case .deviceManager: return .deviceManager
             case .apsManager: return .apsManager
             case .apsManager: return .apsManager
             case .nightscout: return .nightscout
             case .nightscout: return .nightscout
+            case .remoteControl: return .remoteControl
             }
             }
         }
         }
 
 
@@ -147,6 +150,7 @@ final class Logger {
                  .deviceManager,
                  .deviceManager,
                  .nightscout,
                  .nightscout,
                  .openAPS,
                  .openAPS,
+                 .remoteControl,
                  .service:
                  .service:
                 return OSLog(subsystem: subsystem, category: name)
                 return OSLog(subsystem: subsystem, category: name)
             }
             }

+ 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,

+ 11 - 5
FreeAPS/Sources/Models/DecimalPickerSettings.swift

@@ -18,10 +18,16 @@ class PickerSettingsProvider: ObservableObject {
         }
         }
 
 
         // Glucose values are stored as mg/dl values, so Integers.
         // Glucose values are stored as mg/dl values, so Integers.
-        // Filter out odd numbers to avoid duplicate mmol/L values due to rounding.
+        // Filter out duplicate values when rounded to 1 decimal place.
         if units == .mmolL, setting.type == PickerSetting.PickerSettingType.glucose {
         if units == .mmolL, setting.type == PickerSetting.PickerSettingType.glucose {
-            values = values.filter { Int($0) % 2 == 0 }
+            // Use a Set to track unique values rounded to 1 decimal
+            var uniqueRoundedValues = Set<String>()
+            values = values.filter { value in
+                let roundedValue = String(format: "%.1f", NSDecimalNumber(decimal: value.asMmolL).doubleValue)
+                return uniqueRoundedValues.insert(roundedValue).inserted
+            }
         }
         }
+
         return values
         return values
     }
     }
 }
 }
@@ -110,7 +116,7 @@ struct DecimalPickerSettings {
         value: 0.2,
         value: 0.2,
         step: 0.05,
         step: 0.05,
         min: 0.1,
         min: 0.1,
-        max: 2,
+        max: 0.4,
         type: PickerSetting.PickerSettingType.factor
         type: PickerSetting.PickerSettingType.factor
     )
     )
     var adjustmentFactor = PickerSetting(value: 0.8, step: 0.1, min: 0.5, max: 1.5, type: PickerSetting.PickerSettingType.factor)
     var adjustmentFactor = PickerSetting(value: 0.8, step: 0.1, min: 0.5, max: 1.5, type: PickerSetting.PickerSettingType.factor)
@@ -131,9 +137,9 @@ struct DecimalPickerSettings {
     )
     )
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var delay = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
+    var delay = PickerSetting(value: 60, step: 15, min: 30, max: 120, type: PickerSetting.PickerSettingType.minute)
     var minuteInterval = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
     var minuteInterval = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var timeCap = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.hour)
+    var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 6, step: 0.5, min: 4, max: 10, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 6, step: 0.5, min: 4, max: 10, type: PickerSetting.PickerSettingType.hour)
     var maxBolus = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxBolus = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)

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

@@ -57,6 +57,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
@@ -270,6 +271,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"
+        }
+    }
+}

+ 12 - 0
FreeAPS/Sources/Models/NightscoutStatus.swift

@@ -52,4 +52,16 @@ struct NightscoutProfileStore: JSON {
     let units: String
     let units: String
     let enteredBy: String
     let enteredBy: String
     let store: [String: ScheduledNightscoutProfile]
     let store: [String: ScheduledNightscoutProfile]
+    let bundleIdentifier: String
+    let deviceToken: String
+    let isAPNSProduction: Bool
+    let overridePresets: [NightscoutPresetOverride]?
+    let teamID: String
+}
+
+struct NightscoutPresetOverride: JSON {
+    let name: String
+    let duration: Decimal?
+    let percentage: Double?
+    let target: Decimal?
 }
 }

+ 1 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -77,6 +77,7 @@ enum EventType: String, JSON {
     case nsAnnouncement = "Announcement"
     case nsAnnouncement = "Announcement"
     case nsSensorChange = "Sensor Start"
     case nsSensorChange = "Sensor Start"
     case capillaryGlucose = "BG Check"
     case capillaryGlucose = "BG Check"
+    case note = "Note"
 }
 }
 
 
 enum TempType: String, JSON {
 enum TempType: String, JSON {

+ 141 - 0
FreeAPS/Sources/Models/PushMessage.swift

@@ -0,0 +1,141 @@
+import Foundation
+
+struct PushMessage: Codable, Sendable {
+    var user: String
+    var commandType: TrioRemoteControl.CommandType
+    var bolusAmount: Decimal?
+    var target: Int?
+    var duration: Int?
+    var carbs: Int?
+    var protein: Int?
+    var fat: Int?
+    var sharedSecret: String
+    var timestamp: TimeInterval
+    var overrideName: String?
+    var scheduledTime: TimeInterval?
+
+    enum CodingKeys: String, CodingKey {
+        case aps
+        case user
+        case commandType = "command_type"
+        case bolusAmount = "bolus_amount"
+        case target
+        case duration
+        case carbs
+        case protein
+        case fat
+        case sharedSecret = "shared_secret"
+        case timestamp
+        case overrideName
+        case scheduledTime = "scheduled_time"
+    }
+
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(user, forKey: .user)
+        try container.encode(commandType, forKey: .commandType)
+        try container.encodeIfPresent(bolusAmount, forKey: .bolusAmount)
+        try container.encodeIfPresent(target, forKey: .target)
+        try container.encodeIfPresent(duration, forKey: .duration)
+        try container.encodeIfPresent(carbs, forKey: .carbs)
+        try container.encodeIfPresent(protein, forKey: .protein)
+        try container.encodeIfPresent(fat, forKey: .fat)
+        try container.encode(sharedSecret, forKey: .sharedSecret)
+        try container.encode(timestamp, forKey: .timestamp)
+        try container.encodeIfPresent(overrideName, forKey: .overrideName)
+        if let scheduledTime = scheduledTime {
+            try container.encode(scheduledTime, forKey: .scheduledTime)
+        }
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        user = try container.decode(String.self, forKey: .user)
+        commandType = try container.decode(TrioRemoteControl.CommandType.self, forKey: .commandType)
+        bolusAmount = try container.decodeIfPresent(Decimal.self, forKey: .bolusAmount)
+        target = try container.decodeIfPresent(Int.self, forKey: .target)
+        duration = try container.decodeIfPresent(Int.self, forKey: .duration)
+        carbs = try container.decodeIfPresent(Int.self, forKey: .carbs)
+        protein = try container.decodeIfPresent(Int.self, forKey: .protein)
+        fat = try container.decodeIfPresent(Int.self, forKey: .fat)
+        sharedSecret = try container.decode(String.self, forKey: .sharedSecret)
+        timestamp = try container.decode(TimeInterval.self, forKey: .timestamp)
+        overrideName = try container.decodeIfPresent(String.self, forKey: .overrideName)
+        scheduledTime = try container.decodeIfPresent(TimeInterval.self, forKey: .scheduledTime)
+    }
+
+    init(
+        user: String,
+        commandType: TrioRemoteControl.CommandType,
+        bolusAmount: Decimal? = nil,
+        target: Int? = nil,
+        duration: Int? = nil,
+        carbs: Int? = nil,
+        protein: Int? = nil,
+        fat: Int? = nil,
+        sharedSecret: String,
+        timestamp: TimeInterval,
+        overrideName: String? = nil,
+        scheduledTime: TimeInterval? = nil
+    ) {
+        self.user = user
+        self.commandType = commandType
+        self.bolusAmount = bolusAmount
+        self.target = target
+        self.duration = duration
+        self.carbs = carbs
+        self.protein = protein
+        self.fat = fat
+        self.sharedSecret = sharedSecret
+        self.timestamp = timestamp
+        self.overrideName = overrideName
+        self.scheduledTime = scheduledTime
+    }
+
+    func humanReadableDescription() -> String {
+        var description = "User: \(user). Command Type: \(commandType.description). "
+
+        if let override = overrideName {
+            description += "Override Name: \(override). "
+        }
+
+        switch commandType {
+        case .bolus:
+            if let amount = bolusAmount {
+                description += "Bolus Amount: \(amount) units."
+            } else {
+                description += "Bolus Amount: unknown."
+            }
+        case .tempTarget:
+            let targetDesc = target != nil ? "\(target!) mg/dL" : "unknown target"
+            let durationDesc = duration != nil ? "\(duration!) minutes" : "unknown duration"
+            description += "Temp Target: \(targetDesc), Duration: \(durationDesc)."
+        case .cancelTempTarget:
+            description += "Cancel Temp Target command."
+        case .meal:
+            let carbsDesc = carbs != nil ? "\(carbs!)g carbs" : "unknown carbs"
+            let fatDesc = fat != nil ? "\(fat!)g fat" : "unknown fat"
+            let proteinDesc = protein != nil ? "\(protein!)g protein" : "unknown protein"
+            description += "Meal with \(carbsDesc), \(fatDesc), \(proteinDesc)."
+        case .startOverride:
+            if let override = overrideName {
+                description += "Start Override: \(override)."
+            } else {
+                description += "Start Override: unknown override name."
+            }
+        case .cancelOverride:
+            description += "Cancel Override command."
+        }
+
+        if let scheduledTime = scheduledTime {
+            let date = Date(timeIntervalSince1970: scheduledTime)
+            let formatter = DateFormatter()
+            formatter.dateStyle = .short
+            formatter.timeStyle = .short
+            let dateString = formatter.string(from: date)
+            description += " Scheduled for: \(dateString)."
+        }
+
+        return description
+    }
+}

+ 22 - 21
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -1,28 +1,29 @@
 import Combine
 import Combine
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension AlgorithmAdvancedSettings {
 extension AlgorithmAdvancedSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
-        @Injected() var nightscout: NightscoutManager!
-
-        @Published var units: GlucoseUnits = .mgdL
-
-        @Published var maxDailySafetyMultiplier: Decimal = 3
-        @Published var currentBasalSafetyMultiplier: Decimal = 4
-        @Published var useCustomPeakTime: Bool = false
-        @Published var insulinPeakTime: Decimal = 75
-        @Published var skipNeutralTemps: Bool = false
-        @Published var unsuspendIfNoTemp: Bool = false
-        @Published var suspendZerosIOB: Bool = false
-        @Published var min5mCarbimpact: Decimal = 8
-        @Published var autotuneISFAdjustmentFraction: Decimal = 1.0
-        @Published var remainingCarbsFraction: Decimal = 1.0
-        @Published var remainingCarbsCap: Decimal = 90
-        @Published var noisyCGMTargetMultiplier: Decimal = 1.3
-
-        @Published var insulinActionCurve: Decimal = 6
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
+        @ObservationIgnored @Injected() var nightscout: NightscoutManager!
+
+        var units: GlucoseUnits = .mgdL
+
+        var maxDailySafetyMultiplier: Decimal = 3
+        var currentBasalSafetyMultiplier: Decimal = 4
+        var useCustomPeakTime: Bool = false
+        var insulinPeakTime: Decimal = 75
+        var skipNeutralTemps: Bool = false
+        var unsuspendIfNoTemp: Bool = false
+        var suspendZerosIOB: Bool = false
+        var min5mCarbimpact: Decimal = 8
+        var autotuneISFAdjustmentFraction: Decimal = 1.0
+        var remainingCarbsFraction: Decimal = 1.0
+        var remainingCarbsCap: Decimal = 90
+        var noisyCGMTargetMultiplier: Decimal = 1.3
+
+        var insulinActionCurve: Decimal = 6
 
 
         var preferences: Preferences {
         var preferences: Preferences {
             settingsManager.preferences
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension AlgorithmAdvancedSettings {
 extension AlgorithmAdvancedSettings {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?
         @State var selectedVerboseHint: String?

+ 8 - 7
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -1,15 +1,16 @@
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension AutosensSettings {
 extension AutosensSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
 
 
-        @Published var units: GlucoseUnits = .mgdL
+        var units: GlucoseUnits = .mgdL
 
 
-        @Published var autosensMax: Decimal = 1.2
-        @Published var autosensMin: Decimal = 0.7
-        @Published var rewindResetsAutosens: Bool = true
+        var autosensMax: Decimal = 1.2
+        var autosensMin: Decimal = 0.7
+        var rewindResetsAutosens: Bool = true
 
 
         var preferences: Preferences {
         var preferences: Preferences {
             settingsManager.preferences
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension AutosensSettings {
 extension AutosensSettings {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?
         @State var selectedVerboseHint: String?

+ 9 - 8
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -1,14 +1,15 @@
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension BasalProfileEditor {
 extension BasalProfileEditor {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() private var nightscout: NightscoutManager!
-
-        @Published var syncInProgress: Bool = false
-        @Published var initialItems: [Item] = []
-        @Published var items: [Item] = []
-        @Published var total: Decimal = 0.0
-        @Published var showAlert: Bool = false
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
+
+        var syncInProgress: Bool = false
+        var initialItems: [Item] = []
+        var items: [Item] = []
+        var total: Decimal = 0.0
+        var showAlert: Bool = false
 
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
 

+ 1 - 1
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension BasalProfileEditor {
 extension BasalProfileEditor {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var editMode = EditMode.inactive
         @State private var editMode = EditMode.inactive
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme

+ 126 - 121
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -2,121 +2,122 @@ import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
+import Observation
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
 extension Bolus {
 extension Bolus {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var unlockmanager: UnlockManager!
-        @Injected() var apsManager: APSManager!
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var pumpHistoryStorage: PumpHistoryStorage!
-        // added for bolus calculator
-        @Injected() var settings: SettingsManager!
-        @Injected() var nsManager: NightscoutManager!
-        @Injected() var carbsStorage: CarbsStorage!
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var determinationStorage: DeterminationStorage!
-
-        @Published var lowGlucose: Decimal = 70
-        @Published var highGlucose: Decimal = 180
-
-        @Published var predictions: Predictions?
-        @Published var amount: Decimal = 0
-        @Published var insulinRecommended: Decimal = 0
-        @Published var insulinRequired: Decimal = 0
-        @Published var units: GlucoseUnits = .mgdL
-        @Published var threshold: Decimal = 0
-        @Published var maxBolus: Decimal = 0
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var nsManager: NightscoutManager!
+        @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+
+        var lowGlucose: Decimal = 70
+        var highGlucose: Decimal = 180
+        var glucoseColorScheme: GlucoseColorScheme = .staticColor
+
+        var predictions: Predictions?
+        var amount: Decimal = 0
+        var insulinRecommended: Decimal = 0
+        var insulinRequired: Decimal = 0
+        var units: GlucoseUnits = .mgdL
+        var threshold: Decimal = 0
+        var maxBolus: Decimal = 0
         var maxExternal: Decimal { maxBolus * 3 }
         var maxExternal: Decimal { maxBolus * 3 }
-        @Published var errorString: Decimal = 0
-        @Published var evBG: Decimal = 0
-        @Published var insulin: Decimal = 0
-        @Published var isf: Decimal = 0
-        @Published var error: Bool = false
-        @Published var minGuardBG: Decimal = 0
-        @Published var minDelta: Decimal = 0
-        @Published var expectedDelta: Decimal = 0
-        @Published var minPredBG: Decimal = 0
-        @Published var waitForSuggestion: Bool = false
-        @Published var carbRatio: Decimal = 0
-
-        @Published var addButtonPressed: Bool = false
+        var errorString: Decimal = 0
+        var evBG: Decimal = 0
+        var insulin: Decimal = 0
+        var isf: Decimal = 0
+        var error: Bool = false
+        var minGuardBG: Decimal = 0
+        var minDelta: Decimal = 0
+        var expectedDelta: Decimal = 0
+        var minPredBG: Decimal = 0
+        var waitForSuggestion: Bool = false
+        var carbRatio: Decimal = 0
+
+        var addButtonPressed: Bool = false
 
 
         var waitForSuggestionInitial: Bool = false
         var waitForSuggestionInitial: Bool = false
 
 
-        // added for bolus calculator
-        @Published var target: Decimal = 0
-        @Published var cob: Int16 = 0
-        @Published var iob: Decimal = 0
-
-        @Published var currentBG: Decimal = 0
-        @Published var fifteenMinInsulin: Decimal = 0
-        @Published var deltaBG: Decimal = 0
-        @Published var targetDifferenceInsulin: Decimal = 0
-        @Published var targetDifference: Decimal = 0
-        @Published var wholeCob: Decimal = 0
-        @Published var wholeCobInsulin: Decimal = 0
-        @Published var iobInsulinReduction: Decimal = 0
-        @Published var wholeCalc: Decimal = 0
-        @Published var insulinCalculated: Decimal = 0
-        @Published var fraction: Decimal = 0
-        @Published var basal: Decimal = 0
-        @Published var fattyMeals: Bool = false
-        @Published var fattyMealFactor: Decimal = 0
-        @Published var useFattyMealCorrectionFactor: Bool = false
-        @Published var displayPresets: Bool = true
-
-        @Published var currentBasal: Decimal = 0
-        @Published var currentCarbRatio: Decimal = 0
-        @Published var currentBGTarget: Decimal = 0
-        @Published var currentISF: Decimal = 0
-
-        @Published var sweetMeals: Bool = false
-        @Published var sweetMealFactor: Decimal = 0
-        @Published var useSuperBolus: Bool = false
-        @Published var superBolusInsulin: Decimal = 0
-
-        @Published var meal: [CarbsEntry]?
-        @Published var carbs: Decimal = 0
-        @Published var fat: Decimal = 0
-        @Published var protein: Decimal = 0
-        @Published var note: String = ""
-
-        @Published var date = Date()
-
-        @Published var carbsRequired: Decimal?
-        @Published var useFPUconversion: Bool = false
-        @Published var dish: String = ""
-        @Published var selection: MealPresetStored?
-        @Published var summation: [String] = []
-        @Published var maxCarbs: Decimal = 0
-        @Published var maxFat: Decimal = 0
-        @Published var maxProtein: Decimal = 0
-
-        @Published var id_: String = ""
-        @Published var summary: String = ""
-
-        @Published var externalInsulin: Bool = false
-        @Published var showInfo: Bool = false
-        @Published var glucoseFromPersistence: [GlucoseStored] = []
-        @Published var determination: [OrefDetermination] = []
-        @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
-        @Published var predictionsForChart: Predictions?
-        @Published var simulatedDetermination: Determination?
-        @Published var determinationObjectIDs: [NSManagedObjectID] = []
-
-        @Published var minForecast: [Int] = []
-        @Published var maxForecast: [Int] = []
-        @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 isSmoothingEnabled: Bool = false
-        @Published var stops: [Gradient.Stop] = []
+        var target: Decimal = 0
+        var cob: Int16 = 0
+        var iob: Decimal = 0
+
+        var currentBG: Decimal = 0
+        var fifteenMinInsulin: Decimal = 0
+        var deltaBG: Decimal = 0
+        var targetDifferenceInsulin: Decimal = 0
+        var targetDifference: Decimal = 0
+        var wholeCob: Decimal = 0
+        var wholeCobInsulin: Decimal = 0
+        var iobInsulinReduction: Decimal = 0
+        var wholeCalc: Decimal = 0
+        var insulinCalculated: Decimal = 0
+        var fraction: Decimal = 0
+        var basal: Decimal = 0
+        var fattyMeals: Bool = false
+        var fattyMealFactor: Decimal = 0
+        var useFattyMealCorrectionFactor: Bool = false
+        var displayPresets: Bool = true
+
+        var currentBasal: Decimal = 0
+        var currentCarbRatio: Decimal = 0
+        var currentBGTarget: Decimal = 0
+        var currentISF: Decimal = 0
+
+        var sweetMeals: Bool = false
+        var sweetMealFactor: Decimal = 0
+        var useSuperBolus: Bool = false
+        var superBolusInsulin: Decimal = 0
+
+        var meal: [CarbsEntry]?
+        var carbs: Decimal = 0
+        var fat: Decimal = 0
+        var protein: Decimal = 0
+        var note: String = ""
+
+        var date = Date()
+
+        var carbsRequired: Decimal?
+        var useFPUconversion: Bool = false
+        var dish: String = ""
+        var selection: MealPresetStored?
+        var summation: [String] = []
+        var maxCarbs: Decimal = 0
+        var maxFat: Decimal = 0
+        var maxProtein: Decimal = 0
+
+        var id_: String = ""
+        var summary: String = ""
+
+        var externalInsulin: Bool = false
+        var showInfo: Bool = false
+        var glucoseFromPersistence: [GlucoseStored] = []
+        var determination: [OrefDetermination] = []
+        var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
+        var predictionsForChart: Predictions?
+        var simulatedDetermination: Determination?
+        var determinationObjectIDs: [NSManagedObjectID] = []
+
+        var minForecast: [Int] = []
+        var maxForecast: [Int] = []
+        var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        var forecastDisplayType: ForecastDisplayType = .cone
+        var isSmoothingEnabled: Bool = false
+        var stops: [Gradient.Stop] = []
 
 
         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 coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var subscriptions = Set<AnyCancellable>()
         private var subscriptions = Set<AnyCancellable>()
@@ -129,6 +130,8 @@ extension Bolus {
                     .receive(on: DispatchQueue.global(qos: .background))
                     .receive(on: DispatchQueue.global(qos: .background))
                     .share()
                     .share()
                     .eraseToAnyPublisher()
                     .eraseToAnyPublisher()
+            registerHandlers()
+            registerSubscribers()
             setupBolusStateConcurrently()
             setupBolusStateConcurrently()
         }
         }
 
 
@@ -136,12 +139,6 @@ extension Bolus {
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
-                        self.registerHandlers()
-                    }
-                    group.addTask {
-                        self.registerSubscribers()
-                    }
-                    group.addTask {
                         self.setupGlucoseArray()
                         self.setupGlucoseArray()
                     }
                     }
                     group.addTask {
                     group.addTask {
@@ -230,20 +227,21 @@ extension Bolus {
             sweetMealFactor = settings.settings.sweetMealFactor
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
             displayPresets = settings.settings.displayPresets
             forecastDisplayType = settings.settings.forecastDisplayType
             forecastDisplayType = settings.settings.forecastDisplayType
-            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+            lowGlucose = settingsManager.settings.low
+            highGlucose = settingsManager.settings.high
             maxCarbs = settings.settings.maxCarbs
             maxCarbs = settings.settings.maxCarbs
             maxFat = settings.settings.maxFat
             maxFat = settings.settings.maxFat
             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 {
             let now = Date()
             let now = Date()
             let calendar = Calendar.current
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
             let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm:ss"
+            dateFormatter.dateFormat = "HH:mm"
             dateFormatter.timeZone = TimeZone.current
             dateFormatter.timeZone = TimeZone.current
 
 
             let entries: [(start: String, value: Decimal)]
             let entries: [(start: String, value: Decimal)]
@@ -393,9 +391,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()
                 }
                 }
             }
             }
@@ -613,16 +618,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)
         }
         }
     }
     }
@@ -658,16 +663,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"),

+ 1 - 0
FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift

@@ -1,5 +1,6 @@
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 struct AddMealPresetView: View {
 struct AddMealPresetView: View {

+ 110 - 99
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -17,7 +17,7 @@ extension Bolus {
 
 
         let resolver: Resolver
         let resolver: Resolver
 
 
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
 
         @State private var showPresetSheet = false
         @State private var showPresetSheet = false
         @State private var autofocus: Bool = true
         @State private var autofocus: Bool = true
@@ -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)
                                 .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))
         }
         }

+ 72 - 58
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -3,10 +3,9 @@ import CoreData
 import Foundation
 import Foundation
 import SwiftUI
 import SwiftUI
 
 
-struct ForeCastChart: View {
-    @StateObject var state: Bolus.StateModel
+struct ForecastChart: View {
+    var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
-    @Binding var units: GlucoseUnits
 
 
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
 
 
@@ -23,7 +22,7 @@ struct ForeCastChart: View {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.numberStyle = .decimal
 
 
-        if units == .mmolL {
+        if state.units == .mmolL {
             formatter.maximumFractionDigits = 1
             formatter.maximumFractionDigits = 1
             formatter.minimumFractionDigits = 1
             formatter.minimumFractionDigits = 1
             formatter.roundingMode = .halfUp
             formatter.roundingMode = .halfUp
@@ -35,57 +34,54 @@ struct ForeCastChart: View {
 
 
     var body: some View {
     var body: some View {
         VStack {
         VStack {
-            HStack {
-                HStack {
-                    Text("Added carbs: ")
-                        .font(.footnote)
-                        .fontWeight(.bold)
-                        .foregroundStyle(.orange)
+            forecastChartLabels
+                .padding(.bottom, 8)
 
 
-                    Text("\(state.carbs.description) g")
-                        .font(.footnote)
-                        .foregroundStyle(.orange)
-                }
-                .padding(8)
-                .background {
-                    RoundedRectangle(cornerRadius: 10)
-                        .fill(Color.orange.opacity(0.2))
-                }
+            forecastChart
+        }
+    }
 
 
-                Spacer()
+    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))
+            }
 
 
-                HStack {
-                    Text("Added insulin: ")
-                        .font(.footnote)
-                        .fontWeight(.bold)
-                        .foregroundStyle(.blue)
+            Spacer()
 
 
-                    Text("\(state.amount.description) U")
-                        .font(.footnote)
-                        .foregroundStyle(.blue)
-                }
-                .padding(8)
-                .background {
-                    RoundedRectangle(cornerRadius: 10)
-                        .fill(Color.blue.opacity(0.2))
-                }
+            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
+                            state.units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
                         )
                         )
                         .font(.footnote)
                         .font(.footnote)
                         .foregroundStyle(.primary)
                         .foregroundStyle(.primary)
-                        Text("\(units.rawValue)")
+                        Text("\(state.units.rawValue)")
                             .font(.footnote)
                             .font(.footnote)
                             .foregroundStyle(.secondary)
                             .foregroundStyle(.secondary)
                     }
                     }
@@ -93,11 +89,18 @@ struct ForeCastChart: View {
                     Text("---")
                     Text("---")
                         .font(.footnote)
                         .font(.footnote)
                         .foregroundStyle(.primary)
                         .foregroundStyle(.primary)
-                    Text("\(units.rawValue)")
+                    Text("\(state.units.rawValue)")
                         .font(.footnote)
                         .font(.footnote)
                         .foregroundStyle(.secondary)
                         .foregroundStyle(.secondary)
                 }
                 }
             }
             }
+            .font(.footnote)
+            .foregroundStyle(.primary)
+            .padding(8)
+            .background {
+                RoundedRectangle(cornerRadius: 10)
+                    .fill(Color.primary.opacity(0.2))
+            }
         }
         }
     }
     }
 
 
@@ -115,16 +118,27 @@ struct ForeCastChart: View {
         .chartXAxis { forecastChartXAxis }
         .chartXAxis { forecastChartXAxis }
         .chartXScale(domain: startMarker ... endMarker)
         .chartXScale(domain: startMarker ... endMarker)
         .chartYAxis { forecastChartYAxis }
         .chartYAxis { forecastChartYAxis }
-        .chartYScale(domain: units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
+        .chartYScale(domain: state.units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
         .backport.chartForegroundStyleScale(state: state)
         .backport.chartForegroundStyleScale(state: state)
     }
     }
 
 
     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
+            let targetGlucose = (state.determination.first?.currentTarget ?? state.currentBGTarget as NSDecimalNumber) as Decimal
+
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = state.glucoseColorScheme == .dynamicColor
+
+            let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
+                glucoseValue: Decimal(item.glucose),
+                highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : state.highGlucose,
+                lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : state.lowGlucose,
+                targetGlucose: targetGlucose,
+                glucoseColorScheme: state.glucoseColorScheme
+            )
 
 
             if !state.isSmoothingEnabled {
             if !state.isSmoothingEnabled {
                 PointMark(
                 PointMark(
@@ -132,7 +146,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 +154,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)
                 }
                 }
@@ -163,17 +177,17 @@ struct ForeCastChart: View {
 
 
                 // if distance between respective min and max is 0, provide a default range
                 // if distance between respective min and max is 0, provide a default range
                 if yMinMaxDelta == 0 {
                 if yMinMaxDelta == 0 {
-                    let yMinValue = units == .mgdL ? Decimal(state.minForecast[index] - 1) :
+                    let yMinValue = state.units == .mgdL ? Decimal(state.minForecast[index] - 1) :
                         Decimal(state.minForecast[index] - 1)
                         Decimal(state.minForecast[index] - 1)
                         .asMmolL
                         .asMmolL
-                    let yMaxValue = units == .mgdL ? Decimal(state.minForecast[index] + 1) :
+                    let yMaxValue = state.units == .mgdL ? Decimal(state.minForecast[index] + 1) :
                         Decimal(state.minForecast[index] + 1)
                         Decimal(state.minForecast[index] + 1)
                         .asMmolL
                         .asMmolL
 
 
                     AreaMark(
                     AreaMark(
                         x: .value("Time", xValue <= endMarker ? xValue : endMarker),
                         x: .value("Time", xValue <= endMarker ? xValue : endMarker),
-                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
-                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                        yStart: .value("Min Value", state.units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", state.units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
                     )
                     )
                     .foregroundStyle(Color.blue.opacity(0.5))
                     .foregroundStyle(Color.blue.opacity(0.5))
                     .interpolationMethod(.catmullRom)
                     .interpolationMethod(.catmullRom)
@@ -184,8 +198,8 @@ struct ForeCastChart: View {
 
 
                     AreaMark(
                     AreaMark(
                         x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
                         x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
-                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
-                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                        yStart: .value("Min Value", state.units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", state.units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
                     )
                     )
                     .foregroundStyle(Color.blue.opacity(0.5))
                     .foregroundStyle(Color.blue.opacity(0.5))
                     .interpolationMethod(.catmullRom)
                     .interpolationMethod(.catmullRom)
@@ -210,7 +224,7 @@ struct ForeCastChart: View {
                 ForEach(values.indices, id: \.self) { index in
                 ForEach(values.indices, id: \.self) { index in
                     LineMark(
                     LineMark(
                         x: .value("Time", timeForIndex(Int32(index))),
                         x: .value("Time", timeForIndex(Int32(index))),
-                        y: .value("Value", units == .mgdL ? Decimal(values[index]) : Decimal(values[index]).asMmolL)
+                        y: .value("Value", state.units == .mgdL ? Decimal(values[index]) : Decimal(values[index]).asMmolL)
                     )
                     )
                     .foregroundStyle(by: .value("Prediction Type", name))
                     .foregroundStyle(by: .value("Prediction Type", name))
                 }
                 }
@@ -232,8 +246,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 +255,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)
         }
         }
     }
     }
 }
 }

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift

@@ -3,7 +3,7 @@ import Foundation
 import SwiftUI
 import SwiftUI
 
 
 struct MealPresetView: View {
 struct MealPresetView: View {
-    @StateObject var state: Bolus.StateModel
+    @Bindable var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.dismiss) var dismiss
     @Environment(\.dismiss) var dismiss
     @Environment(\.managedObjectContext) var moc
     @Environment(\.managedObjectContext) var moc

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -1,7 +1,7 @@
 import SwiftUI
 import SwiftUI
 
 
 struct PopupView: View {
 struct PopupView: View {
-    @StateObject var state: Bolus.StateModel
+    var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
 
 
     private var fractionDigits: Int {
     private var fractionDigits: Int {

+ 10 - 10
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -1,21 +1,21 @@
+import Observation
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 
 
 extension Calibrations {
 extension Calibrations {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var calibrationService: CalibrationService!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var calibrationService: CalibrationService!
 
 
-        @Published var slope: Double = 1
-        @Published var intercept: Double = 1
-        @Published var newCalibration: Decimal = 0
-        @Published var calibrations: [Calibration] = []
-        @Published var calibrate: (Int) -> Double = { Double($0) }
-        @Published var items: [Item] = []
+        var slope: Double = 1
+        var intercept: Double = 1
+        var newCalibration: Decimal = 0
+        var calibrations: [Calibration] = []
+        var calibrate: (Int) -> Double = { Double($0) }
+        var items: [Item] = []
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
-        // TODO: - test if we need to use the viewContext here
         private let context = CoreDataStack.shared.newTaskContext()
         private let context = CoreDataStack.shared.newTaskContext()
 
 
         override func subscribe() {
         override func subscribe() {

+ 1 - 1
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift

@@ -1,7 +1,7 @@
 import SwiftUI
 import SwiftUI
 
 
 struct CalibrationsChart: View {
 struct CalibrationsChart: View {
-    @EnvironmentObject var state: Calibrations.StateModel
+    var state: Calibrations.StateModel
 
 
     private var dateFormatter: DateFormatter {
     private var dateFormatter: DateFormatter {
         let formatter = DateFormatter()
         let formatter = DateFormatter()

+ 2 - 2
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension Calibrations {
 extension Calibrations {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
         var color: LinearGradient {
@@ -101,7 +101,7 @@ extension Calibrations {
 
 
                     if state.calibrations.isNotEmpty {
                     if state.calibrations.isNotEmpty {
                         Section(header: Text("Chart")) {
                         Section(header: Text("Chart")) {
-                            CalibrationsChart().environmentObject(state)
+                            CalibrationsChart(state: state)
                                 .frame(minHeight: geo.size.width)
                                 .frame(minHeight: geo.size.width)
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
                     }
                     }

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

+ 139 - 63
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -1,29 +1,31 @@
 import CoreData
 import CoreData
+import HealthKit
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension DataTable {
 extension DataTable {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var apsManager: APSManager!
-        @Injected() var unlockmanager: UnlockManager!
-        @Injected() private var storage: FileStorage!
-        @Injected() var pumpHistoryStorage: PumpHistoryStorage!
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var healthKitManager: HealthKitManager!
-        @Injected() var carbsStorage: CarbsStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
+        @ObservationIgnored @Injected() private var storage: FileStorage!
+        @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var healthKitManager: HealthKitManager!
+        @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
 
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
-        @Published var mode: Mode = .treatments
-        @Published var treatments: [Treatment] = []
-        @Published var glucose: [Glucose] = []
-        @Published var meals: [Treatment] = []
-        @Published var manualGlucose: Decimal = 0
-        @Published var maxBolus: Decimal = 0
-        @Published var waitForSuggestion: Bool = false
+        var mode: Mode = .treatments
+        var treatments: [Treatment] = []
+        var glucose: [Glucose] = []
+        var meals: [Treatment] = []
+        var manualGlucose: Decimal = 0
+        var maxBolus: Decimal = 0
+        var waitForSuggestion: Bool = false
 
 
-        @Published var insulinEntryDeleted: Bool = false
-        @Published var carbEntryDeleted: Bool = false
+        var insulinEntryDeleted: Bool = false
+        var carbEntryDeleted: Bool = false
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
@@ -37,19 +39,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 {
@@ -60,41 +69,55 @@ 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?
 
 
@@ -103,46 +126,66 @@ extension DataTable {
                 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)
+
+                        // Delete Fat and Protein entries from Apple Health
+                        let healthObjectsToDelete: [HKSampleType?] = [
+                            AppleHealthConfig.healthFatObject,
+                            AppleHealthConfig.healthProteinObject
+                        ]
+
+                        for sampleType in healthObjectsToDelete {
+                            if let validSampleType = sampleType {
+                                self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
+                            }
+                        }
                     } else {
                     } 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
+                            )
                         }
                         }
                     }
                     }
 
 
                 } catch {
                 } catch {
                     debugPrint(
                     debugPrint(
-                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from Nightscout: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
                     )
                     )
                 }
                 }
             }
             }
-
-            // Delete carbs from Core Data
-            await carbsStorage.deleteCarbs(treatmentObjectID)
-
-            // Perform a determine basal sync to update cob
-            await apsManager.determineBasalSync()
         }
         }
 
 
         // Insulin deletion from history
         // 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()
 
 
@@ -151,14 +194,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)"
@@ -166,6 +209,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)
@@ -178,6 +252,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 }

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

@@ -5,7 +5,9 @@ import Swinject
 extension DataTable {
 extension DataTable {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+
+        @State var state = StateModel()
+
         @State private var isRemoveHistoryItemAlertPresented: Bool = false
         @State private var isRemoveHistoryItemAlertPresented: Bool = false
         @State private var alertTitle: String = ""
         @State private var alertTitle: String = ""
         @State private var alertMessage: String = ""
         @State private var alertMessage: String = ""

+ 13 - 12
FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift

@@ -1,19 +1,20 @@
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension DynamicSettings {
 extension DynamicSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
 
 
-        @Published var useNewFormula: Bool = false
-        @Published var enableDynamicCR: Bool = false
-        @Published var sigmoid: Bool = false
-        @Published var adjustmentFactor: Decimal = 0.8
-        @Published var adjustmentFactorSigmoid: Decimal = 0.5
-        @Published var weightPercentage: Decimal = 0.65
-        @Published var tddAdjBasal: Bool = false
-        @Published var threshold_setting: Decimal = 60
-        @Published var units: GlucoseUnits = .mgdL
+        var useNewFormula: Bool = false
+        var enableDynamicCR: Bool = false
+        var sigmoid: Bool = false
+        var adjustmentFactor: Decimal = 0.8
+        var adjustmentFactorSigmoid: Decimal = 0.5
+        var weightPercentage: Decimal = 0.65
+        var tddAdjBasal: Bool = false
+        var threshold_setting: Decimal = 60
+        var units: GlucoseUnits = .mgdL
 
 
         var preferences: Preferences {
         var preferences: Preferences {
             settingsManager.preferences
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension DynamicSettings {
 extension DynamicSettings {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?
         @State var selectedVerboseHint: String?

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 18 - 18
FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift


+ 2 - 2
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -32,7 +32,7 @@ extension GlucoseNotificationSettings {
                 addSourceInfoToGlucoseNotifications = $0 }
                 addSourceInfoToGlucoseNotifications = $0 }
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
                 let value = max(min($0, 400), 40)
                 let value = max(min($0, 400), 40)
-                lowGlucose = units == .mmolL ? value.asMmolL : value
+                lowGlucose = value
             }, map: {
             }, map: {
                 guard units == .mmolL else { return $0 }
                 guard units == .mmolL else { return $0 }
                 return $0.asMgdL
                 return $0.asMgdL
@@ -40,7 +40,7 @@ extension GlucoseNotificationSettings {
 
 
             subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
             subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
                 let value = max(min($0, 400), 40)
                 let value = max(min($0, 400), 40)
-                highGlucose = units == .mmolL ? value.asMmolL : value
+                highGlucose = value
             }, map: {
             }, map: {
                 guard units == .mmolL else { return $0 }
                 guard units == .mmolL else { return $0 }
                 return $0.asMgdL
                 return $0.asMgdL

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

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

@@ -0,0 +1,101 @@
+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 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, maxGlucose + 100)
+                let minOverall = min(minGlucose, minForecast ?? minGlucose)
+                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 }
+        }
+    }
+}

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

@@ -0,0 +1,111 @@
+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
+        }
+
+        // Update minCount on the Main Thread
+        minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
+
+        // Safely read minCount for use inside the detached task
+        let localMinCount = minCount
+
+        guard localMinCount > 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 ..< localMinCount).map { index in
+                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
+            }
+
+            let maxForecast = (0 ..< localMinCount).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
+    }
+}

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

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

+ 158 - 562
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -2,93 +2,112 @@ import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKitUI
 import LoopKitUI
+import Observation
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 
 
 extension Home {
 extension Home {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var apsManager: APSManager!
-        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
-        @Injected() var nightscoutManager: NightscoutManager!
-        @Injected() var determinationStorage: DeterminationStorage!
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var carbsStorage: CarbsStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
+        @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @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 announcement: [Announcement] = []
-        @Published var uploadStats = false
-        @Published var recentGlucose: BloodGlucose?
-        @Published var maxBasal: Decimal = 2
-        @Published var autotunedBasalProfile: [BasalProfileEntry] = []
-        @Published var basalProfile: [BasalProfileEntry] = []
-        @Published var tempTargets: [TempTarget] = []
-        @Published var timerDate = Date()
-        @Published var closedLoop = false
-        @Published var pumpSuspended = false
-        @Published var isLooping = false
-        @Published var statusTitle = ""
-        @Published var lastLoopDate: Date = .distantPast
-        @Published var battery: Battery?
-        @Published var reservoir: Decimal?
-        @Published var pumpName = ""
-        @Published var pumpExpiresAtDate: Date?
-        @Published var tempTarget: TempTarget?
-        @Published var setupPump = false
-        @Published var errorMessage: String? = nil
-        @Published var errorDate: Date? = nil
-        @Published var bolusProgress: Decimal?
-        @Published var eventualBG: Int?
-        @Published var allowManualTemp = false
-        @Published var units: GlucoseUnits = .mgdL
-        @Published var pumpDisplayState: PumpDisplayState?
-        @Published var alarm: GlucoseAlarm?
-        @Published var manualTempBasal = false
-        @Published var isSmoothingEnabled = false
-        @Published var maxValue: Decimal = 1.2
-        @Published var lowGlucose: Decimal = 70
-        @Published var highGlucose: Decimal = 180
-        @Published var overrideUnit: Bool = false
-        @Published var displayXgridLines: Bool = false
-        @Published var displayYgridLines: Bool = false
-        @Published var thresholdLines: Bool = false
-        @Published var timeZone: TimeZone?
-        @Published var hours: Int16 = 6
-        @Published var totalBolus: Decimal = 0
-        @Published var isStatusPopupPresented: Bool = false
-        @Published var isLegendPresented: Bool = false
-        @Published var legendSheetDetent = PresentationDetent.large
-        @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
-        @Published var roundedTotalBolus: String = ""
-        @Published var selectedTab: Int = 0
-        @Published var waitForSuggestion: Bool = false
-        @Published var glucoseFromPersistence: [GlucoseStored] = []
-        @Published var latestTwoGlucoseValues: [GlucoseStored] = []
-        @Published var carbsFromPersistence: [CarbEntryStored] = []
-        @Published var fpusFromPersistence: [CarbEntryStored] = []
-        @Published var determinationsFromPersistence: [OrefDetermination] = []
-        @Published var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
-        @Published var insulinFromPersistence: [PumpEventStored] = []
-        @Published var tempBasals: [PumpEventStored] = []
-        @Published var suspensions: [PumpEventStored] = []
-        @Published var batteryFromPersistence: [OpenAPS_Battery] = []
-        @Published var lastPumpBolus: PumpEventStored?
-        @Published var overrides: [OverrideStored] = []
-        @Published var overrideRunStored: [OverrideRunStored] = []
-        @Published var isOverrideCancelled: Bool = false
-        @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
-        @Published var pumpStatusHighlightMessage: String? = nil
-        @Published var cgmAvailable: Bool = false
-        @Published var showCarbsRequiredBadge: Bool = true
+        var manualGlucose: [BloodGlucose] = []
+        var announcement: [Announcement] = []
+        var uploadStats = false
+        var recentGlucose: BloodGlucose?
+        var maxBasal: Decimal = 2
+        var autotunedBasalProfile: [BasalProfileEntry] = []
+        var basalProfile: [BasalProfileEntry] = []
+        var tempTargets: [TempTarget] = []
+        var timerDate = Date()
+        var closedLoop = false
+        var pumpSuspended = false
+        var isLooping = false
+        var statusTitle = ""
+        var lastLoopDate: Date = .distantPast
+        var battery: Battery?
+        var reservoir: Decimal?
+        var pumpName = ""
+        var pumpExpiresAtDate: Date?
+        var tempTarget: TempTarget?
+        var setupPump = false
+        var errorMessage: String?
+        var errorDate: Date?
+        var bolusProgress: Decimal?
+        var eventualBG: Int?
+        var allowManualTemp = false
+        var units: GlucoseUnits = .mgdL
+        var pumpDisplayState: PumpDisplayState?
+        var alarm: GlucoseAlarm?
+        var manualTempBasal = false
+        var isSmoothingEnabled = false
+        var maxValue: Decimal = 1.2
+        var lowGlucose: Decimal = 70
+        var highGlucose: Decimal = 180
+        var currentGlucoseTarget: Decimal = 100
+        var glucoseColorScheme: GlucoseColorScheme = .staticColor
+        var overrideUnit: Bool = false
+        var displayXgridLines: Bool = false
+        var displayYgridLines: Bool = false
+        var thresholdLines: Bool = false
+        var timeZone: TimeZone?
+        var hours: Int16 = 6
+        var totalBolus: Decimal = 0
+        var isStatusPopupPresented: Bool = false
+        var isLegendPresented: Bool = false
+        var legendSheetDetent = PresentationDetent.large
+        var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
+        var roundedTotalBolus: String = ""
+        var selectedTab: Int = 0
+        var waitForSuggestion: Bool = false
+        var glucoseFromPersistence: [GlucoseStored] = []
+        var latestTwoGlucoseValues: [GlucoseStored] = []
+        var carbsFromPersistence: [CarbEntryStored] = []
+        var fpusFromPersistence: [CarbEntryStored] = []
+        var determinationsFromPersistence: [OrefDetermination] = []
+        var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
+        var insulinFromPersistence: [PumpEventStored] = []
+        var tempBasals: [PumpEventStored] = []
+        var suspensions: [PumpEventStored] = []
+        var batteryFromPersistence: [OpenAPS_Battery] = []
+        var lastPumpBolus: PumpEventStored?
+        var overrides: [OverrideStored] = []
+        var overrideRunStored: [OverrideRunStored] = []
+        var isOverrideCancelled: Bool = false
+        var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
+        var pumpStatusHighlightMessage: String?
+        var cgmAvailable: Bool = false
+        var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
 
 
-        @Published var minForecast: [Int] = []
-        @Published var maxForecast: [Int] = []
-        @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
+        var minForecast: [Int] = []
+        var maxForecast: [Int] = []
+        var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        var forecastDisplayType: ForecastDisplayType = .cone
 
 
-        let context = CoreDataStack.shared.newTaskContext()
+        var minYAxisValue: Decimal = 39
+        var maxYAxisValue: Decimal = 300
+
+        var minValueCobChart: Decimal = 0
+        var maxValueCobChart: Decimal = 20
+
+        var minValueIobChart: Decimal = 0
+        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 coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
@@ -103,6 +122,9 @@ extension Home {
                     .share()
                     .share()
                     .eraseToAnyPublisher()
                     .eraseToAnyPublisher()
 
 
+            registerSubscribers()
+            registerHandlers()
+
             // Parallelize Setup functions
             // Parallelize Setup functions
             setupHomeViewConcurrently()
             setupHomeViewConcurrently()
         }
         }
@@ -111,12 +133,6 @@ extension Home {
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
-                        self.registerSubscribers()
-                    }
-                    group.addTask {
-                        self.registerHandlers()
-                    }
-                    group.addTask {
                         self.setupGlucoseArray()
                         self.setupGlucoseArray()
                     }
                     }
                     group.addTask {
                     group.addTask {
@@ -313,9 +329,10 @@ 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
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+            lowGlucose = settingsManager.settings.low
+            highGlucose = settingsManager.settings.high
             overrideUnit = settingsManager.settings.overrideHbA1cUnit
             overrideUnit = settingsManager.settings.overrideHbA1cUnit
             displayXgridLines = settingsManager.settings.xGridLines
             displayXgridLines = settingsManager.settings.xGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             displayYgridLines = settingsManager.settings.yGridLines
@@ -375,7 +392,7 @@ extension Home {
                 guard viewContext.hasChanges else { return }
                 guard viewContext.hasChanges else { return }
                 try viewContext.save()
                 try viewContext.save()
 
 
-                Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
             } catch {
             } catch {
                 debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
                 debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
             }
             }
@@ -478,6 +495,54 @@ extension Home {
             }
             }
         }
         }
 
 
+        private func getCurrentGlucoseTarget() async {
+            let now = Date()
+            let calendar = Calendar.current
+            let dateFormatter = DateFormatter()
+            dateFormatter.dateFormat = "HH:mm"
+            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 = entry.value
+                    }
+                    return
+                }
+            }
+        }
+
         func openCGM() {
         func openCGM() {
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
         }
         }
@@ -519,9 +584,13 @@ extension Home.StateModel:
         units = settingsManager.settings.units
         units = settingsManager.settings.units
         manualTempBasal = apsManager.isManualTempBasal
         manualTempBasal = apsManager.isManualTempBasal
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
-        lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-        highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+        lowGlucose = settingsManager.settings.low
+        highGlucose = settingsManager.settings.high
+        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
@@ -588,476 +657,3 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // TODO:
         // TODO:
     }
     }
 }
 }
-
-// MARK: - Handle Core Data changes and update Arrays to display them in the UI
-
-extension Home.StateModel {
-    // Setup Glucose
-    private func setupGlucoseArray() {
-        Task {
-            let ids = await self.fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateGlucoseArray(with: glucoseObjects)
-        }
-    }
-
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.glucose,
-            key: "date",
-            ascending: true,
-            fetchLimit: 288
-        )
-
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
-        glucoseFromPersistence = objects
-        latestTwoGlucoseValues = Array(objects.suffix(2))
-    }
-
-    // Setup Carbs
-    private func setupCarbsArray() {
-        Task {
-            let ids = await self.fetchCarbs()
-            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateCarbsArray(with: carbObjects)
-        }
-    }
-
-    private func fetchCarbs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: context,
-            predicate: NSPredicate.carbsForChart,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
-        carbsFromPersistence = objects
-    }
-
-    // Setup FPUs
-    private func setupFPUsArray() {
-        Task {
-            let ids = await self.fetchFPUs()
-            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateFPUsArray(with: fpuObjects)
-        }
-    }
-
-    private func fetchFPUs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: context,
-            predicate: NSPredicate.fpusForChart,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
-        fpusFromPersistence = objects
-    }
-
-    // Custom fetch to more efficiently filter only for cob and iob
-    private func fetchCobAndIob() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: context,
-            predicate: NSPredicate.determinationsForCobIobCharts,
-            key: "deliverAt",
-            ascending: false,
-            batchSize: 50,
-            propertiesToFetch: ["cob", "iob", "objectID"]
-        )
-
-        guard let fetchedResults = results as? [[String: Any]] else {
-            return []
-        }
-
-        return await context.perform {
-            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
-        }
-    }
-
-    // Setup Determinations
-    private func setupDeterminationsArray() {
-        Task {
-            // Get the NSManagedObjectIDs
-            async let enactedObjectIDs = determinationStorage
-                .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
-            async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
-
-            let enactedIDs = await enactedObjectIDs
-            let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
-
-            // Get the NSManagedObjects and return them on the Main Thread
-            await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
-            await updateDeterminationsArray(with: enactedAndNonEnactedIDs, keyPath: \.enactedAndNonEnactedDeterminations)
-
-            await updateForecastData()
-        }
-    }
-
-    @MainActor private func updateDeterminationsArray(
-        with IDs: [NSManagedObjectID],
-        keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) async {
-        // Fetch the objects off the main thread
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: IDs, context: viewContext)
-
-        // Update the array on the main thread
-        self[keyPath: keyPath] = determinationObjects
-    }
-
-    // Setup Insulin
-    private func setupInsulinArray() {
-        Task {
-            let ids = await self.fetchInsulin()
-            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateInsulinArray(with: insulinObjects)
-        }
-    }
-
-    private func fetchInsulin() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: context,
-            predicate: NSPredicate.pumpHistoryLast24h,
-            key: "timestamp",
-            ascending: true
-        )
-
-        guard let pumpEvents = results as? [PumpEventStored] else {
-            return []
-        }
-
-        return await context.perform {
-            return pumpEvents.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
-        insulinFromPersistence = insulinObjects
-
-        // Filter tempbasals
-        manualTempBasal = apsManager.isManualTempBasal
-        tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
-
-        // Suspension and resume events
-        suspensions = insulinFromPersistence.filter {
-            $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
-        }
-        let lastSuspension = suspensions.last
-
-        pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
-            .type == EventType.pumpSuspend.rawValue
-    }
-
-    // Setup Last Bolus to display the bolus progress bar
-    // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
-    private func setupLastBolus() {
-        Task {
-            guard let id = await self.fetchLastBolus() else { return }
-            await updateLastBolus(with: id)
-        }
-    }
-
-    private func fetchLastBolus() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: context,
-            predicate: NSPredicate.lastPumpBolus,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1
-        )
-
-        guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID).first
-        }
-    }
-
-    @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
-        do {
-            lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
-        } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
-            )
-        }
-    }
-
-    // Setup Battery
-    private func setupBatteryArray() {
-        Task {
-            let ids = await self.fetchBattery()
-            let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateBatteryArray(with: batteryObjects)
-        }
-    }
-
-    private func fetchBattery() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OpenAPS_Battery.self,
-            onContext: context,
-            predicate: NSPredicate.predicateFor30MinAgo,
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
-        batteryFromPersistence = objects
-    }
-}
-
-extension Home.StateModel {
-    // Setup Overrides
-    private func setupOverrides() {
-        Task {
-            let ids = await self.fetchOverrides()
-            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideArray(with: overrideObjects)
-        }
-    }
-
-    private func fetchOverrides() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideStored.self,
-            onContext: context,
-            predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
-            key: "date",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
-        overrides = objects
-    }
-
-    @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
-        guard let overrideDuration = override.duration as? Double, overrideDuration != 0 else {
-            return TimeInterval(60 * 60 * 24) // one day
-        }
-        return TimeInterval(overrideDuration * 60) // return seconds
-    }
-
-    @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
-        guard let overrideTarget = override.target, overrideTarget != 0 else {
-            return 100 // default
-        }
-        return overrideTarget.decimalValue
-    }
-
-    // Setup expired Overrides
-    private func setupOverrideRunStored() {
-        Task {
-            let ids = await self.fetchOverrideRunStored()
-            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideRunStoredArray(with: overrideRunObjects)
-        }
-    }
-
-    private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
-        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideRunStored.self,
-            onContext: context,
-            predicate: predicate,
-            key: "startDate",
-            ascending: false
-        )
-
-        guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
-
-        return await context.perform {
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
-        overrideRunStored = objects
-    }
-
-    @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
-    }
-}

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

@@ -25,17 +25,17 @@ extension MainChartView {
                 drawTempBasals(dummy: false)
                 drawTempBasals(dummy: false)
                 drawBasalProfile()
                 drawBasalProfile()
                 drawSuspensions()
                 drawSuspensions()
-            }.onChange(of: state.tempBasals) { _ in
+            }.onChange(of: state.tempBasals) {
                 calculateBasals()
                 calculateBasals()
                 calculateTempBasalsInBackground()
                 calculateTempBasalsInBackground()
             }
             }
-            .onChange(of: state.maxBasal) { _ in
+            .onChange(of: state.maxBasal) {
                 calculateBasals()
                 calculateBasals()
             }
             }
-            .onChange(of: state.autotunedBasalProfile) { _ in
+            .onChange(of: state.autotunedBasalProfile) {
                 calculateBasals()
                 calculateBasals()
             }
             }
-            .onChange(of: state.basalProfile) { _ in
+            .onChange(of: state.basalProfile) {
                 calculateBasals()
                 calculateBasals()
             }
             }
             .frame(minHeight: geo.size.height * 0.05)
             .frame(minHeight: geo.size.height * 0.05)

+ 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 {

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

@@ -7,10 +7,33 @@ extension MainChartView {
     var staticYAxisChart: some View {
     var staticYAxisChart: some View {
         Chart {
         Chart {
             /// high and low threshold lines
             /// high and low threshold lines
+
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
             if thresholdLines {
             if thresholdLines {
-                RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
+                let highColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: highGlucose,
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme
+                )
+                let lowColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: lowGlucose,
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme
+                )
+
+                RuleMark(y: .value("High", units == .mgdL ? highGlucose : highGlucose.asMmolL))
+                    .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", units == .mgdL ? lowGlucose : lowGlucose.asMmolL))
+                    .foregroundStyle(lowColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
             }
             }
         }
         }
@@ -21,7 +44,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 +74,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
+
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
+            let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
+                glucoseValue: Decimal(item.glucose),
+                highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                targetGlucose: currentGlucoseTarget,
+                glucoseColorScheme: glucoseColorScheme
+            )
 
 
             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)
         }
         }
     }
     }

+ 47 - 120
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -7,17 +7,18 @@ let calendar = Calendar.current
 
 
 struct MainChartView: View {
 struct MainChartView: View {
     var geo: GeometryProxy
     var geo: GeometryProxy
-    @Binding var units: GlucoseUnits
-    @Binding var hours: Int
-    @Binding var tempTargets: [TempTarget]
-    @Binding var highGlucose: Decimal
-    @Binding var lowGlucose: Decimal
-    @Binding var screenHours: Int16
-    @Binding var displayXgridLines: Bool
-    @Binding var displayYgridLines: Bool
-    @Binding var thresholdLines: Bool
-
-    @StateObject var state: Home.StateModel
+    var units: GlucoseUnits
+    var hours: Int
+    var tempTargets: [TempTarget]
+    var highGlucose: Decimal
+    var lowGlucose: Decimal
+    var currentGlucoseTarget: Decimal
+    var glucoseColorScheme: GlucoseColorScheme
+    var screenHours: Int16
+    var displayXgridLines: Bool
+    var displayYgridLines: Bool
+    var thresholdLines: Bool
+    var state: Home.StateModel
 
 
     @State var basalProfiles: [BasalProfile] = []
     @State var basalProfiles: [BasalProfile] = []
     @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
     @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
@@ -25,13 +26,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
@@ -100,33 +97,26 @@ struct MainChartView: View {
                                 iobChart
                                 iobChart
                             }
                             }
 
 
-                        }.onChange(of: screenHours) { _ in
+                        }.onChange(of: screenHours) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
                         }
-                        .onChange(of: state.glucoseFromPersistence.last?.glucose) { _ in
-                            updateStartEndMarkers()
-                            yAxisChartData()
+                        .onChange(of: state.glucoseFromPersistence.last?.glucose) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             scroller.scrollTo("MainChart", anchor: .trailing)
+                            updateStartEndMarkers()
                         }
                         }
-                        .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) { _ in
-                            yAxisChartDataCobChart()
-                            yAxisChartDataIobChart()
+                        .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
                         }
-                        .onChange(of: units) { _ in
-                            yAxisChartData()
-                            yAxisChartDataCobChart()
-                            yAxisChartDataIobChart()
+                        .onChange(of: units) {
+                            // 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 +142,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 +158,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 +173,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
                 )
                 )
 
 
@@ -226,10 +218,10 @@ extension MainChartView {
                 }
                 }
             }
             }
             .id("MainChart")
             .id("MainChart")
-            .onChange(of: state.insulinFromPersistence) { _ in
+            .onChange(of: state.insulinFromPersistence) {
                 state.roundedTotalBolus = state.calculateTINS()
                 state.roundedTotalBolus = state.calculateTINS()
             }
             }
-            .onChange(of: tempTargets) { _ in
+            .onChange(of: tempTargets) {
                 Task {
                 Task {
                     await calculateTempTargets()
                     await calculateTempTargets()
                 }
                 }
@@ -241,14 +233,16 @@ 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)
         }
         }
     }
     }
 
 
     @ViewBuilder var selectionPopover: some View {
     @ViewBuilder var selectionPopover: some View {
         if let sgv = selectedGlucose?.glucose {
         if let sgv = selectedGlucose?.glucose {
-            let glucoseToShow = units == .mgdL ? Decimal(sgv) : Decimal(sgv).asMmolL
             VStack(alignment: .leading) {
             VStack(alignment: .leading) {
                 HStack {
                 HStack {
                     Image(systemName: "clock")
                     Image(systemName: "clock")
@@ -256,13 +250,24 @@ extension MainChartView {
                         .font(.body).bold()
                         .font(.body).bold()
                 }.font(.body).padding(.bottom, 5)
                 }.font(.body).padding(.bottom, 5)
 
 
+                // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                let hardCodedLow = Decimal(55)
+                let hardCodedHigh = Decimal(220)
+                let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
+                let glucoseColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(sgv),
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme
+                )
                 HStack {
                 HStack {
-                    Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
+                    Text(units == .mgdL ? Decimal(sgv).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 +416,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 {

+ 35 - 45
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -2,14 +2,15 @@ import CoreData
 import SwiftUI
 import SwiftUI
 
 
 struct CurrentGlucoseView: View {
 struct CurrentGlucoseView: View {
-    @Binding var timerDate: Date
-    @Binding var units: GlucoseUnits
-    @Binding var alarm: GlucoseAlarm?
-    @Binding var lowGlucose: Decimal
-    @Binding var highGlucose: Decimal
-    @Binding var cgmAvailable: Bool
-
-    var glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
+    let timerDate: Date
+    let units: GlucoseUnits
+    let alarm: GlucoseAlarm?
+    let lowGlucose: Decimal
+    let highGlucose: Decimal
+    let cgmAvailable: Bool
+    var currentGlucoseTarget: Decimal
+    let glucoseColorScheme: GlucoseColorScheme
+    let glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
 
 
     @State private var rotationDegrees: Double = 0.0
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [
     @State private var angularGradient = AngularGradient(colors: [
@@ -79,15 +80,33 @@ 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(
+
+                            var glucoseDisplayColor = Color.primary
+
+                            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                            let hardCodedLow = Decimal(55)
+                            let hardCodedHigh = Decimal(220)
+                            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
+                            if Decimal(glucoseValue) <= lowGlucose || Decimal(glucoseValue) >= highGlucose {
+                                glucoseDisplayColor = FreeAPS.getDynamicGlucoseColor(
+                                    glucoseValue: Decimal(glucoseValue),
+                                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                                    targetGlucose: currentGlucoseTarget,
+                                    glucoseColorScheme: glucoseColorScheme
+                                )
+                            }
+
+                            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 +118,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:
@@ -159,35 +178,6 @@ struct CurrentGlucoseView: View {
         let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
         let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
         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
-        }
-    }
 }
 }
 
 
 struct Triangle: Shape {
 struct Triangle: Shape {

+ 6 - 6
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -8,13 +8,13 @@ struct LoopView: View {
         static let lag: TimeInterval = 30
         static let lag: TimeInterval = 30
     }
     }
 
 
-    @Binding var closedLoop: Bool
-    @Binding var timerDate: Date
-    @Binding var isLooping: Bool
-    @Binding var lastLoopDate: Date
-    @Binding var manualTempBasal: Bool
+    let closedLoop: Bool
+    let timerDate: Date
+    let isLooping: Bool
+    let lastLoopDate: Date
+    let manualTempBasal: Bool
 
 
-    var determination: [OrefDetermination]
+    let determination: [OrefDetermination]
 
 
     private var dateFormatter: DateFormatter {
     private var dateFormatter: DateFormatter {
         let formatter = DateFormatter()
         let formatter = DateFormatter()

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

@@ -2,13 +2,13 @@ import CoreData
 import SwiftUI
 import SwiftUI
 
 
 struct PumpView: View {
 struct PumpView: View {
-    @Binding var reservoir: Decimal?
-    @Binding var name: String
-    @Binding var expiresAtDate: Date?
-    @Binding var timerDate: Date
-    @Binding var timeZone: TimeZone?
-    @Binding var pumpStatusHighlightMessage: String?
-    @Binding var battery: [OpenAPS_Battery]
+    let reservoir: Decimal?
+    let name: String
+    let expiresAtDate: Date?
+    let timerDate: Date
+    let timeZone: TimeZone?
+    let pumpStatusHighlightMessage: String?
+    let battery: [OpenAPS_Battery]
 
 
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
 
 

+ 38 - 33
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -8,7 +8,7 @@ extension Home {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
 
 
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State var isStatusPopupPresented = false
         @State var isStatusPopupPresented = false
         @State var showCancelAlert = false
         @State var showCancelAlert = false
         @State var isMenuPresented = false
         @State var isMenuPresented = false
@@ -299,12 +299,14 @@ extension Home {
 
 
         var glucoseView: some View {
         var glucoseView: some View {
             CurrentGlucoseView(
             CurrentGlucoseView(
-                timerDate: $state.timerDate,
-                units: $state.units,
-                alarm: $state.alarm,
-                lowGlucose: $state.lowGlucose,
-                highGlucose: $state.highGlucose,
-                cgmAvailable: $state.cgmAvailable,
+                timerDate: state.timerDate,
+                units: state.units,
+                alarm: state.alarm,
+                lowGlucose: state.lowGlucose,
+                highGlucose: state.highGlucose,
+                cgmAvailable: state.cgmAvailable,
+                currentGlucoseTarget: state.currentGlucoseTarget,
+                glucoseColorScheme: state.glucoseColorScheme,
                 glucose: state.latestTwoGlucoseValues
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
             ).scaleEffect(0.9)
                 .onTapGesture {
                 .onTapGesture {
@@ -319,13 +321,13 @@ extension Home {
 
 
         var pumpView: some View {
         var pumpView: some View {
             PumpView(
             PumpView(
-                reservoir: $state.reservoir,
-                name: $state.pumpName,
-                expiresAtDate: $state.pumpExpiresAtDate,
-                timerDate: $state.timerDate,
-                timeZone: $state.timeZone,
-                pumpStatusHighlightMessage: $state.pumpStatusHighlightMessage,
-                battery: $state.batteryFromPersistence
+                reservoir: state.reservoir,
+                name: state.pumpName,
+                expiresAtDate: state.pumpExpiresAtDate,
+                timerDate: state.timerDate,
+                timeZone: state.timeZone,
+                pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
+                battery: state.batteryFromPersistence
             ).onTapGesture {
             ).onTapGesture {
                 if state.pumpDisplayState == nil {
                 if state.pumpDisplayState == nil {
                     // shows user confirmation dialog with pump model choices, then proceeds to setup
                     // shows user confirmation dialog with pump model choices, then proceeds to setup
@@ -505,15 +507,17 @@ extension Home {
             ZStack {
             ZStack {
                 MainChartView(
                 MainChartView(
                     geo: geo,
                     geo: geo,
-                    units: $state.units,
-                    hours: .constant(state.filteredHours),
-                    tempTargets: $state.tempTargets,
-                    highGlucose: $state.highGlucose,
-                    lowGlucose: $state.lowGlucose,
-                    screenHours: $state.hours,
-                    displayXgridLines: $state.displayXgridLines,
-                    displayYgridLines: $state.displayYgridLines,
-                    thresholdLines: $state.thresholdLines,
+                    units: state.units,
+                    hours: state.filteredHours,
+                    tempTargets: state.tempTargets,
+                    highGlucose: state.highGlucose,
+                    lowGlucose: state.lowGlucose,
+                    currentGlucoseTarget: state.currentGlucoseTarget,
+                    glucoseColorScheme: state.glucoseColorScheme,
+                    screenHours: state.hours,
+                    displayXgridLines: state.displayXgridLines,
+                    displayYgridLines: state.displayYgridLines,
+                    thresholdLines: state.thresholdLines,
                     state: state
                     state: state
                 )
                 )
             }
             }
@@ -530,11 +534,11 @@ extension Home {
             VStack(alignment: .leading, spacing: 20) {
             VStack(alignment: .leading, spacing: 20) {
                 /// Loop view at bottomLeading
                 /// Loop view at bottomLeading
                 LoopView(
                 LoopView(
-                    closedLoop: $state.closedLoop,
-                    timerDate: $state.timerDate,
-                    isLooping: $state.isLooping,
-                    lastLoopDate: $state.lastLoopDate,
-                    manualTempBasal: $state.manualTempBasal,
+                    closedLoop: state.closedLoop,
+                    timerDate: state.timerDate,
+                    isLooping: state.isLooping,
+                    lastLoopDate: state.lastLoopDate,
+                    manualTempBasal: state.manualTempBasal,
                     determination: state.determinationsFromPersistence
                     determination: state.determinationsFromPersistence
                 ).onTapGesture {
                 ).onTapGesture {
                     state.isStatusPopupPresented = true
                     state.isStatusPopupPresented = true
@@ -596,8 +600,9 @@ extension Home {
                         .foregroundColor(.loopYellow)
                         .foregroundColor(.loopYellow)
                     Text(
                     Text(
                         (
                         (
-                            numberFormatter
-                                .string(from: (state.enactedAndNonEnactedDeterminations.first?.cob ?? 0) as NSNumber) ?? "0"
+                            numberFormatter.string(
+                                from: NSNumber(value: state.enactedAndNonEnactedDeterminations.first?.cob ?? 0)
+                            ) ?? "0"
                         ) +
                         ) +
                             NSLocalizedString(" g", comment: "gram of carbs")
                             NSLocalizedString(" g", comment: "gram of carbs")
                     )
                     )
@@ -644,7 +649,7 @@ extension Home {
                                 NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
                                 NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
                         )
                         )
                         .font(.system(size: 16, weight: .bold, design: .rounded))
                         .font(.system(size: 16, weight: .bold, design: .rounded))
-                        .onChange(of: state.hours) { _ in
+                        .onChange(of: state.hours) {
                             state.roundedTotalBolus = state.calculateTINS()
                             state.roundedTotalBolus = state.calculateTINS()
                         }
                         }
                         .onAppear {
                         .onAppear {
@@ -967,7 +972,7 @@ extension Home {
                     }
                     }
                 }
                 }
             }
             }
-            .onChange(of: state.hours) { _ in
+            .onChange(of: state.hours) {
                 highlightButtons()
                 highlightButtons()
             }
             }
             .onAppear {
             .onAppear {
@@ -1138,7 +1143,7 @@ extension Home {
                     }
                     }
                 )
                 )
             }.ignoresSafeArea(.keyboard, edges: .bottom).blur(radius: state.waitForSuggestion ? 8 : 0)
             }.ignoresSafeArea(.keyboard, edges: .bottom).blur(radius: state.waitForSuggestion ? 8 : 0)
-                .onChange(of: selectedTab) { _ in
+                .onChange(of: selectedTab) {
                     print("current path is empty: \(settingsPath.isEmpty)")
                     print("current path is empty: \(settingsPath.isEmpty)")
                     settingsPath = NavigationPath()
                     settingsPath = NavigationPath()
                 }
                 }

+ 9 - 8
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -1,18 +1,19 @@
 import CoreData
 import CoreData
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension ISFEditor {
 extension ISFEditor {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var determinationStorage: DeterminationStorage!
-        @Injected() private var nightscout: NightscoutManager!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+        @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
 
 
-        @Published var items: [Item] = []
-        @Published var initialItems: [Item] = []
-        @Published var shouldDisplaySaving: Bool = false
+        var items: [Item] = []
+        var initialItems: [Item] = []
+        var shouldDisplaySaving: Bool = false
         private(set) var autosensISF: Decimal?
         private(set) var autosensISF: Decimal?
         private(set) var autosensRatio: Decimal = 0
         private(set) var autosensRatio: Decimal = 0
-        @Published var autotune: Autotune?
-        @Published var determinationsFromPersistence: [OrefDetermination] = []
+        var autotune: Autotune?
+        var determinationsFromPersistence: [OrefDetermination] = []
 
 
         let context = CoreDataStack.shared.newTaskContext()
         let context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext

+ 1 - 1
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension ISFEditor {
 extension ISFEditor {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var editMode = EditMode.inactive
         @State private var editMode = EditMode.inactive
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme

+ 0 - 3
FreeAPS/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift

@@ -3,16 +3,13 @@ import SwiftUI
 
 
 extension LiveActivitySettings {
 extension LiveActivitySettings {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
         @Injected() var storage: FileStorage!
         @Injected() var storage: FileStorage!
 
 
         @Published var units: GlucoseUnits = .mgdL
         @Published var units: GlucoseUnits = .mgdL
         @Published var useLiveActivity = false
         @Published var useLiveActivity = false
         @Published var lockScreenView: LockScreenView = .simple
         @Published var lockScreenView: LockScreenView = .simple
-
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
-
             subscribeSetting(\.useLiveActivity, on: $useLiveActivity) { useLiveActivity = $0 }
             subscribeSetting(\.useLiveActivity, on: $useLiveActivity) { useLiveActivity = $0 }
             subscribeSetting(\.lockScreenView, on: $lockScreenView) { lockScreenView = $0 }
             subscribeSetting(\.lockScreenView, on: $lockScreenView) { lockScreenView = $0 }
         }
         }

+ 12 - 0
FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift

@@ -115,6 +115,18 @@ extension LiveActivitySettings {
                                     ).buttonStyle(BorderlessButtonStyle())
                                     ).buttonStyle(BorderlessButtonStyle())
                                 }.padding(.top)
                                 }.padding(.top)
                             }.padding(.bottom)
                             }.padding(.bottom)
+
+                            if state.lockScreenView == .detailed {
+                                HStack {
+                                    NavigationLink(
+                                        "Widget Configuration",
+                                        destination: LiveActivityWidgetConfiguration(
+                                            resolver: resolver,
+                                            state: state
+                                        )
+                                    ).foregroundStyle(Color.accentColor)
+                                }
+                            }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
                     }
                     }
                 }
                 }

+ 388 - 0
FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift

@@ -0,0 +1,388 @@
+import Charts
+import Foundation
+import SwiftUI
+import Swinject
+import UniformTypeIdentifiers
+
+struct LiveActivityWidgetConfiguration: BaseView {
+    let resolver: Resolver
+
+    @ObservedObject var state: LiveActivitySettings.StateModel
+
+    @State private var selectedItems: [LiveActivityItem?] = Array(repeating: nil, count: 4)
+    @State private var showAddItemDialog: Bool = false
+    @State private var buttonIndexToUpdate: Int?
+    @State private var itemToRemove: LiveActivityItem?
+    @State private var isRemovalConfirmationPresented: Bool = false
+    @State private var glucoseData: [DummyGlucoseData] = []
+
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
+
+    private var color: LinearGradient {
+        colorScheme == .dark ? LinearGradient(
+            gradient: Gradient(colors: [
+                Color.bgDarkBlue,
+                Color.bgDarkerDarkBlue
+            ]),
+            startPoint: .top,
+            endPoint: .bottom
+        )
+            :
+            LinearGradient(
+                gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
+                startPoint: .top,
+                endPoint: .bottom
+            )
+    }
+
+    private func generateDummyGlucoseData() -> [DummyGlucoseData] {
+        var data = [DummyGlucoseData]()
+        let totalMinutes = 6 * 60
+        let interval = 5
+
+        var glucoseLevel: Double = 90 // Start at a normal fasting glucose level
+
+        for minute in stride(from: 0, to: totalMinutes, by: interval) {
+            let time = Double(minute) / 60.0 // Convert minutes to hours
+
+            let trendFactor: Double
+            let randomFactor = Double.random(in: -5 ... 5) // Add slight randomness to each point
+
+            // Simulate different phases during the 6-hour window
+            if time < 1 { // Stable glucose (pre-meal or fasting period)
+                trendFactor = 0.5 + randomFactor // Small increase with some variability
+            } else if time >= 1, time < 2 { // Glucose rising (e.g., post-meal spike)
+                trendFactor = 3.0 + randomFactor // Rapid increase with slight variation
+            } else if time >= 2, time < 3.5 { // Peak and plateau
+                trendFactor = -0.1 + randomFactor // Gradual decrease after the peak with variability
+            } else if time >= 3.5, time < 4.5 { // Second peak (optional, simulate another meal)
+                trendFactor = 2.5 + randomFactor // Another spike with some randomness
+            } else { // Post-meal decrease (insulin effect)
+                trendFactor = -1.5 + randomFactor // Glucose decreasing gradually with some variability
+            }
+
+            // Calculate the next glucose level with trend factors
+            glucoseLevel += trendFactor
+
+            // Ensure glucose level doesn't go out of realistic bounds:
+            glucoseLevel = max(70, min(glucoseLevel, 200))
+
+            data.append(DummyGlucoseData(time: Double(minute), glucoseLevel: Int(glucoseLevel.rounded())))
+        }
+        return data
+    }
+
+    var body: some View {
+        VStack {
+            Group {
+                VStack(alignment: .trailing, spacing: 0) {
+                    Text("Live Activity Personalization".uppercased())
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                        .foregroundColor(.secondary)
+                        .font(.footnote)
+                        .padding(.leading)
+                }
+            }.padding(.bottom, -15)
+
+            GroupBox {
+                VStack {
+                    dummyChart(glucoseData)
+
+                    HStack(spacing: 15) {
+                        ForEach(0 ..< 4, id: \.self) { index in
+                            widgetButton(for: index)
+                        }
+                    }
+                    .padding()
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 12)
+                            .stroke(style: StrokeStyle(lineWidth: 2, dash: [5]))
+                            .foregroundColor(.gray)
+                    )
+                    .cornerRadius(12)
+                }
+
+            }.padding(.vertical).groupBoxStyle(.dummyChart)
+
+            Group {
+                HStack {
+                    Image(systemName: "info.circle")
+                    Text(
+                        "To re-order widgets, remove them and re-add them in the desired order."
+                    )
+                }
+            }.frame(maxWidth: .infinity, alignment: .leading)
+                .foregroundColor(.secondary)
+                .font(.footnote)
+                .padding(.horizontal)
+
+            Spacer()
+        }
+        .padding()
+        .scrollContentBackground(.hidden).background(color)
+        .navigationTitle("Widget Configuration")
+        .navigationBarTitleDisplayMode(.automatic)
+        .onAppear {
+            if glucoseData.isEmpty {
+                glucoseData = generateDummyGlucoseData()
+            }
+            loadOrder() // Load the saved order when the view appears
+        }
+        .confirmationDialog("Add Widget", isPresented: $showAddItemDialog, titleVisibility: .visible) {
+            ForEach(LiveActivityItem.allCases.filter { !selectedItems.contains($0) }, id: \.self) { item in
+                Button(item.displayName) {
+                    if let index = buttonIndexToUpdate {
+                        addItem(item, at: index)
+                    }
+                }
+            }
+        }
+    }
+
+    @ViewBuilder private func widgetButton(for index: Int) -> some View {
+        if index < selectedItems.count, let selectedItem = selectedItems[index] {
+            // Display selected item preview
+            ZStack(alignment: .topTrailing) {
+                getItemPreview(for: selectedItem)
+                    .frame(width: 50, height: 50)
+                    .padding(5)
+                    .background(Color.clear)
+                    .cornerRadius(12)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 12)
+                            .stroke(Color.primary, lineWidth: 1)
+                    )
+                Button(action: {
+                    isRemovalConfirmationPresented = true
+                    itemToRemove = selectedItem
+                }) {
+                    Image(systemName: "trash.circle.fill")
+                        .foregroundColor(Color(UIColor.systemGray2))
+                        .background(Color.white)
+                        .clipShape(Circle())
+                        .font(.system(size: 20))
+                }
+                .offset(x: 10, y: -10)
+                .confirmationDialog("Remove Widget", isPresented: $isRemovalConfirmationPresented, titleVisibility: .hidden) {
+                    Button("Remove Widget", role: .destructive) {
+                        if let itemToRemove = itemToRemove {
+                            removeItem(itemToRemove)
+                        }
+                    }
+                }
+            }
+        } else {
+            // Show "+" symbol for empty slots
+            Button(action: {
+                buttonIndexToUpdate = index
+                showAddItemDialog.toggle()
+            }) {
+                VStack {
+                    Image(systemName: "plus")
+                        .font(.title2)
+                        .foregroundColor(.accentColor)
+                }
+                .frame(width: 50, height: 50)
+                .padding(5)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 12)
+                        .stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
+                        .foregroundColor(.primary)
+                )
+            }
+            .buttonStyle(.plain)
+        }
+    }
+
+    private func getItemPreview(for item: LiveActivityItem) -> some View {
+        switch item {
+        case .currentGlucose:
+            return AnyView(currentGlucosePreview)
+        case .cob:
+            return AnyView(cobPreview)
+        case .iob:
+            return AnyView(iobPreview)
+        case .updatedLabel:
+            return AnyView(updatedLabelPreview)
+        }
+    }
+
+    @ViewBuilder private func dummyChart(_ glucoseData: [DummyGlucoseData]) -> some View {
+        Chart {
+            ForEach(glucoseData) { data in
+                let pointMarkColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(data.glucoseLevel),
+                    highGlucoseColorValue: !(state.settingsManager.settings.glucoseColorScheme == .dynamicColor) ? state
+                        .settingsManager.settings.highGlucose : Decimal(220),
+                    lowGlucoseColorValue: !(state.settingsManager.settings.glucoseColorScheme == .dynamicColor) ? state
+                        .settingsManager.settings.lowGlucose : Decimal(55),
+                    targetGlucose: Decimal(100),
+                    glucoseColorScheme: state.settingsManager.settings.glucoseColorScheme
+                )
+
+                PointMark(
+                    x: .value("Time", data.time),
+                    y: .value("Glucose Level", data.glucoseLevel)
+                ).foregroundStyle(pointMarkColor).symbolSize(15)
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { _ in
+                AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
+                AxisValueLabel().foregroundStyle(.primary).font(.footnote)
+            }
+        }
+        .chartYScale(domain: 39 ... 200)
+        .chartYAxis(.hidden)
+        .chartPlotStyle { plotContent in
+            plotContent
+                .background(
+                    RoundedRectangle(cornerRadius: 12)
+                        .fill(Color.cyan.opacity(0.15))
+                )
+                .clipShape(RoundedRectangle(cornerRadius: 12))
+        }
+        .chartXAxis {
+            AxisMarks(position: .automatic) { _ in
+                AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.primary)
+            }
+        }
+        .frame(height: 100)
+    }
+
+    private var currentGlucosePreview: some View {
+        VStack {
+            HStack(alignment: .center) {
+                Text("123")
+                    .fontWeight(.bold)
+                    .font(.caption)
+            }
+            HStack(spacing: -5) {
+                HStack {
+                    Text("\u{2192}")
+                    Text("+6")
+                }.foregroundStyle(.primary).font(.caption2)
+            }
+        }
+    }
+
+    private var cobPreview: some View {
+        VStack(spacing: 2) {
+            Text("25 g").fontWeight(.bold).font(.caption)
+            Text("COB").font(.caption2).foregroundStyle(.primary)
+        }
+    }
+
+    private var iobPreview: some View {
+        VStack(spacing: 2) {
+            Text("2 U").fontWeight(.bold).font(.caption)
+            Text("IOB").font(.caption2).foregroundStyle(.primary)
+        }
+    }
+
+    private var updatedLabelPreview: some View {
+        VStack {
+            Text("19:05")
+                .fontWeight(.bold)
+                .font(.caption)
+                .foregroundStyle(.primary)
+
+            Text("Updated").font(.caption2).foregroundStyle(.primary)
+        }
+    }
+
+    private func loadOrder() {
+        if let savedItems = UserDefaults.standard.loadLiveActivityOrder() {
+            selectedItems = savedItems.count == 4 ? savedItems : savedItems + Array(repeating: nil, count: 4 - savedItems.count)
+        } else {
+            selectedItems = LiveActivityItem.defaultItems
+            saveOrder()
+        }
+    }
+
+    private func saveOrder() {
+        UserDefaults.standard.saveLiveActivityOrder(selectedItems)
+        Foundation.NotificationCenter.default.post(name: .liveActivityOrderDidChange, object: nil)
+    }
+
+    private func addItem(_ item: LiveActivityItem, at index: Int) {
+        selectedItems[index] = item
+        saveOrder()
+    }
+
+    private func removeItem(_ item: LiveActivityItem) {
+        if let index = selectedItems.firstIndex(of: item) {
+            selectedItems[index] = nil
+            saveOrder()
+        }
+    }
+}
+
+// Extension for UserDefaults to save and load the order
+extension UserDefaults {
+    private enum Keys {
+        static let liveActivityOrder = "liveActivityOrder"
+    }
+
+    func saveLiveActivityOrder(_ items: [LiveActivityItem?]) {
+        let itemStrings = items.map { $0?.rawValue ?? "" }
+        set(itemStrings, forKey: Keys.liveActivityOrder)
+    }
+
+    func loadLiveActivityOrder() -> [LiveActivityItem?]? {
+        if let itemStrings = array(forKey: Keys.liveActivityOrder) as? [String] {
+            return itemStrings.map { $0.isEmpty ? nil : LiveActivityItem(rawValue: $0) }
+        }
+        return nil
+    }
+}
+
+// Enum to represent each live activity item
+enum LiveActivityItem: String, CaseIterable, Identifiable {
+    case currentGlucose
+    case iob
+    case cob
+    case updatedLabel
+
+    var id: String { rawValue }
+
+    static var defaultItems: [LiveActivityItem] {
+        [.currentGlucose, .iob, .cob, .updatedLabel]
+    }
+
+    var displayName: String {
+        switch self {
+        case .currentGlucose:
+            return "Current Glucose"
+        case .iob:
+            return "IOB"
+        case .cob:
+            return "COB"
+        case .updatedLabel:
+            return "Last Updated"
+        }
+    }
+}
+
+struct DummyGlucoseData: Identifiable {
+    let id = UUID()
+    let time: Double // Time in hours
+    let glucoseLevel: Int // Glucose level in mg/dL
+}
+
+struct DummyChartGroupBoxStyle: GroupBoxStyle {
+    func makeBody(configuration: Configuration) -> some View {
+        VStack {
+            configuration.content
+        }
+        .padding()
+        .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
+        .background(Color.chart, in: RoundedRectangle(cornerRadius: 12))
+        .frame(width: UIScreen.main.bounds.width * 0.9)
+    }
+}
+
+extension GroupBoxStyle where Self == DummyChartGroupBoxStyle {
+    static var dummyChart: DummyChartGroupBoxStyle { .init() }
+}

+ 5 - 4
FreeAPS/Sources/Modules/ManualTempBasal/ManualTempBasalStateModel.swift

@@ -1,10 +1,11 @@
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension ManualTempBasal {
 extension ManualTempBasal {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var apsManager: APSManager!
-        @Published var rate: Decimal = 0
-        @Published var durationIndex = 0
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        var rate: Decimal = 0
+        var durationIndex = 0
 
 
         let durationValues = stride(from: 30.0, to: 720.1, by: 30.0).map { $0 }
         let durationValues = stride(from: 30.0, to: 720.1, by: 30.0).map { $0 }
 
 

+ 1 - 1
FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension ManualTempBasal {
 extension ManualTempBasal {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
         var color: LinearGradient {

+ 3 - 6
FreeAPS/Sources/Modules/MealSettings/MealSettingsStateModel.swift

@@ -20,22 +20,19 @@ extension MealSettings {
             subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
             subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
             subscribeSetting(\.maxProtein, on: $maxProtein) { maxProtein = $0 }
             subscribeSetting(\.maxProtein, on: $maxProtein) { maxProtein = $0 }
             subscribeSetting(\.timeCap, on: $timeCap.map(Int.init), initial: {
             subscribeSetting(\.timeCap, on: $timeCap.map(Int.init), initial: {
-                let value = max(min($0, 12), 5)
-                timeCap = Decimal(value)
+                timeCap = Decimal($0)
             }, map: {
             }, map: {
                 $0
                 $0
             })
             })
 
 
             subscribeSetting(\.minuteInterval, on: $minuteInterval.map(Int.init), initial: {
             subscribeSetting(\.minuteInterval, on: $minuteInterval.map(Int.init), initial: {
-                let value = max(min($0, 60), 10)
-                minuteInterval = Decimal(value)
+                minuteInterval = Decimal($0)
             }, map: {
             }, map: {
                 $0
                 $0
             })
             })
 
 
             subscribeSetting(\.delay, on: $delay.map(Int.init), initial: {
             subscribeSetting(\.delay, on: $delay.map(Int.init), initial: {
-                let value = max(min($0, 120), 60)
-                delay = Decimal(value)
+                delay = Decimal($0)
             }, map: {
             }, map: {
                 $0
                 $0
             })
             })

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

@@ -67,6 +67,22 @@ extension NightscoutConfig {
             importedInsulinActionCurve = pumpSettings.insulinActionCurve
             importedInsulinActionCurve = pumpSettings.insulinActionCurve
 
 
             isConnectedToNS = nightscoutAPI != nil
             isConnectedToNS = nightscoutAPI != nil
+
+            $isUploadEnabled
+                .dropFirst()
+                .removeDuplicates()
+                .sink { [weak self] enabled in
+                    guard let self = self else { return }
+                    if enabled {
+                        debug(.nightscout, "Upload has been enabled by the user.")
+                        Task {
+                            await self.nightscoutManager.uploadProfiles()
+                        }
+                    } else {
+                        debug(.nightscout, "Upload has been disabled by the user.")
+                    }
+                }
+                .store(in: &lifetime)
         }
         }
 
 
         func connect() {
         func connect() {
@@ -326,12 +342,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))
                         }
                         }

+ 63 - 54
FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift

@@ -1,61 +1,66 @@
+import Combine
 import CoreData
 import CoreData
+import Observation
 import SwiftUI
 import SwiftUI
 
 
 extension OverrideConfig {
 extension OverrideConfig {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var storage: TempTargetsStorage!
-        @Injected() var apsManager: APSManager!
-        @Injected() var overrideStorage: OverrideStorage!
-
-        @Published var overrideSliderPercentage: Double = 100
-        @Published var isEnabled = false
-        @Published var indefinite = true
-        @Published var overrideDuration: Decimal = 0
-        @Published var target: Decimal = 0
-        @Published var shouldOverrideTarget: Bool = false
-        @Published var smbIsOff: Bool = false
-        @Published var id = ""
-        @Published var overrideName: String = ""
-        @Published var isPreset: Bool = false
-        @Published var overridePresets: [OverrideStored] = []
-        @Published var advancedSettings: Bool = false
-        @Published var isfAndCr: Bool = true
-        @Published var isf: Bool = true
-        @Published var cr: Bool = true
-        @Published var smbIsAlwaysOff: Bool = false
-        @Published var start: Decimal = 0
-        @Published var end: Decimal = 23
-        @Published var smbMinutes: Decimal = 0
-        @Published var uamMinutes: Decimal = 0
-        @Published var defaultSmbMinutes: Decimal = 0
-        @Published var defaultUamMinutes: Decimal = 0
-        @Published var selectedTab: Tab = .overrides
-        @Published var activeOverrideName: String = ""
-        @Published var currentActiveOverride: OverrideStored?
-        @Published var showOverrideEditSheet = false
-        @Published var showInvalidTargetAlert = false
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var storage: TempTargetsStorage!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
+
+        var overrideSliderPercentage: Double = 100
+        var isEnabled = false
+        var indefinite = true
+        var overrideDuration: Decimal = 0
+        var target: Decimal = 0
+        var shouldOverrideTarget: Bool = false
+        var smbIsOff: Bool = false
+        var id = ""
+        var overrideName: String = ""
+        var isPreset: Bool = false
+        var overridePresets: [OverrideStored] = []
+        var advancedSettings: Bool = false
+        var isfAndCr: Bool = true
+        var isf: Bool = true
+        var cr: Bool = true
+        var smbIsAlwaysOff: Bool = false
+        var start: Decimal = 0
+        var end: Decimal = 23
+        var smbMinutes: Decimal = 0
+        var uamMinutes: Decimal = 0
+        var defaultSmbMinutes: Decimal = 0
+        var defaultUamMinutes: Decimal = 0
+        var selectedTab: Tab = .overrides
+        var activeOverrideName: String = ""
+        var currentActiveOverride: OverrideStored?
+        var showOverrideEditSheet = false
+        var showInvalidTargetAlert = false
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
         // temp target stuff
         // temp target stuff
-        @Published var low: Decimal = 0
-        @Published var high: Decimal = 0
-        @Published var durationTT: Decimal = 0
-        @Published var date = Date()
-        @Published var newPresetName = ""
-        @Published var presetsTT: [TempTarget] = []
-        @Published var percentageTT = 100.0
-        @Published var maxValue: Decimal = 1.2
-        @Published var viewPercantage = false
-        @Published var hbt: Double = 160
-        @Published var didSaveSettings: Bool = false
+        var low: Decimal = 0
+        var high: Decimal = 0
+        var durationTT: Decimal = 0
+        var date = Date()
+        var newPresetName = ""
+        var presetsTT: [TempTarget] = []
+        var percentageTT = 100.0
+        var maxValue: Decimal = 1.2
+        var viewPercantage = false
+        var hbt: Double = 160
+        var didSaveSettings: Bool = false
 
 
         var alertMessage: String {
         var alertMessage: String {
             let target: String = units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
             let target: String = units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
             return "Please enter a valid target between" + " \(target)."
             return "Please enter a valid target between" + " \(target)."
         }
         }
 
 
+        private var cancellables = Set<AnyCancellable>()
+
         override func subscribe() {
         override func subscribe() {
             setupNotification()
             setupNotification()
             units = settingsManager.settings.units
             units = settingsManager.settings.units
@@ -96,16 +101,12 @@ extension OverrideConfig {
 extension OverrideConfig.StateModel {
 extension OverrideConfig.StateModel {
     // Custom Notification to update View when an Override has been cancelled via Home View
     // Custom Notification to update View when an Override has been cancelled via Home View
     func setupNotification() {
     func setupNotification() {
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleOverrideConfigurationUpdate),
-            name: .didUpdateOverrideConfiguration,
-            object: nil
-        )
-    }
-
-    @objc private func handleOverrideConfigurationUpdate() {
-        updateLatestOverrideConfiguration()
+        Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.updateLatestOverrideConfiguration()
+            }
+            .store(in: &cancellables)
     }
     }
 
 
     // MARK: - Enact Overrides
     // MARK: - Enact Overrides
@@ -123,6 +124,10 @@ extension OverrideConfig.StateModel {
 
 
             // Update Presets View
             // Update Presets View
             setupOverridePresetsArray()
             setupOverridePresetsArray()
+
+            Task {
+                await nightscoutManager.uploadProfiles()
+            }
         } catch {
         } catch {
             debugPrint(
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
@@ -286,6 +291,8 @@ extension OverrideConfig.StateModel {
 
 
         // Update Presets View
         // Update Presets View
         setupOverridePresetsArray()
         setupOverridePresetsArray()
+
+        await nightscoutManager.uploadProfiles()
     }
     }
 
 
     // MARK: - Setup Override Presets Array
     // MARK: - Setup Override Presets Array
@@ -318,6 +325,8 @@ extension OverrideConfig.StateModel {
 
 
         // Update Presets View
         // Update Presets View
         setupOverridePresetsArray()
         setupOverridePresetsArray()
+
+        await nightscoutManager.uploadProfiles()
     }
     }
 
 
     // MARK: - Setup the State variables with the last Override configuration
     // MARK: - Setup the State variables with the last Override configuration

+ 6 - 3
FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift

@@ -2,10 +2,11 @@ import Foundation
 import SwiftUI
 import SwiftUI
 
 
 struct EditOverrideForm: View {
 struct EditOverrideForm: View {
+    @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
     @ObservedObject var override: OverrideStored
     @ObservedObject var override: OverrideStored
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
-    @StateObject var state: OverrideConfig.StateModel
+    @Bindable var state: OverrideConfig.StateModel
 
 
     @State private var name: String
     @State private var name: String
     @State private var percentage: Double
     @State private var percentage: Double
@@ -30,7 +31,7 @@ struct EditOverrideForm: View {
 
 
     init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
     init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
         override = overrideToEdit
         override = overrideToEdit
-        _state = StateObject(wrappedValue: state)
+        _state = Bindable(wrappedValue: state)
         _name = State(initialValue: overrideToEdit.name ?? "")
         _name = State(initialValue: overrideToEdit.name ?? "")
         _percentage = State(initialValue: overrideToEdit.percentage)
         _percentage = State(initialValue: overrideToEdit.percentage)
         _indefinite = State(initialValue: overrideToEdit.indefinite)
         _indefinite = State(initialValue: overrideToEdit.indefinite)
@@ -294,7 +295,9 @@ struct EditOverrideForm: View {
                         guard let moc = override.managedObjectContext else { return }
                         guard let moc = override.managedObjectContext else { return }
                         guard moc.hasChanges else { return }
                         guard moc.hasChanges else { return }
                         try moc.save()
                         try moc.save()
-
+                        Task {
+                            await nightscoutManager.uploadProfiles()
+                        }
                         if let currentActiveOverride = state.currentActiveOverride {
                         if let currentActiveOverride = state.currentActiveOverride {
                             Task {
                             Task {
                                 await state.disableAllActiveOverrides(
                                 await state.disableAllActiveOverrides(

+ 1 - 1
FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift

@@ -6,7 +6,7 @@ extension OverrideConfig {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
 
 
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
 
         @State private var isEditing = false
         @State private var isEditing = false
         @State private var showOverrideCreationSheet = false
         @State private var showOverrideCreationSheet = false

+ 36 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -0,0 +1,36 @@
+import Foundation
+
+extension TrioRemoteControl {
+    internal func handleAPNSChanges(deviceToken: String?) async {
+        let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
+        let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
+
+        let isAPNSProduction = isRunningInAPNSProductionEnvironment()
+        var shouldUploadProfiles = false
+
+        if let token = deviceToken, token != previousDeviceToken {
+            UserDefaults.standard.set(token, forKey: "deviceToken")
+            debug(.remoteControl, "Device token updated: \(token)")
+            shouldUploadProfiles = true
+        }
+
+        if previousIsAPNSProduction != isAPNSProduction {
+            UserDefaults.standard.set(isAPNSProduction, forKey: "isAPNSProduction")
+            debug(.remoteControl, "APNS environment changed to: \(isAPNSProduction ? "Production" : "Sandbox")")
+            shouldUploadProfiles = true
+        }
+
+        if shouldUploadProfiles {
+            await nightscoutManager.uploadProfiles()
+        } else {
+            debug(.remoteControl, "No changes detected in device token or APNS environment.")
+        }
+    }
+
+    private func isRunningInAPNSProductionEnvironment() -> Bool {
+        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
+            return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
+        }
+        return false
+    }
+}

+ 108 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -0,0 +1,108 @@
+import Foundation
+
+extension TrioRemoteControl {
+    internal func handleBolusCommand(_ pushMessage: PushMessage) async {
+        guard let bolusAmount = pushMessage.bolusAmount else {
+            await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let maxBolus = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.pumpSettings.maxBolus ?? Decimal(0)
+
+        if bolusAmount > maxBolus {
+            await logError(
+                "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let maxIOB = settings.preferences.maxIOB
+        let currentIOB = await fetchCurrentIOB()
+        if (currentIOB + bolusAmount) > maxIOB {
+            await logError(
+                "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let totalRecentBolusAmount = await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+
+        if totalRecentBolusAmount >= bolusAmount * 0.2 {
+            await logError(
+                "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        debug(.remoteControl, "Enacting bolus command with amount: \(bolusAmount) units.")
+
+        guard let apsManager = await FreeAPSApp.resolver.resolve(APSManager.self) else {
+            await logError(
+                "Error: unable to process bolus command because the APS Manager is not available.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        await apsManager.enactBolus(amount: Double(truncating: bolusAmount as NSNumber), isSMB: false)
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    private func fetchCurrentIOB() async -> Decimal {
+        let predicate = NSPredicate.predicateFor30MinAgoForDetermination
+
+        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["iob"]
+        )
+
+        guard let fetchedResults = determinations as? [[String: Any]],
+              let firstResult = fetchedResults.first,
+              let iob = firstResult["iob"] as? Decimal
+        else {
+            await logError("Failed to fetch current IOB.")
+            return Decimal(0)
+        }
+
+        return iob
+    }
+
+    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
+        let predicate = NSPredicate(
+            format: "type == %@ AND timestamp > %@",
+            PumpEventStored.EventType.bolus.rawValue,
+            date as NSDate
+        )
+
+        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: true,
+            fetchLimit: nil,
+            propertiesToFetch: ["bolus.amount"]
+        )
+
+        guard let bolusDictionaries = results as? [[String: Any]] else {
+            await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
+            return 0
+        }
+
+        let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
+
+        return totalAmount
+    }
+}

+ 12 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Helpers.swift

@@ -0,0 +1,12 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func logError(_ errorMessage: String, pushMessage: PushMessage? = nil) async {
+        var note = errorMessage
+        if let pushMessage = pushMessage {
+            note += " Details: \(pushMessage.humanReadableDescription())"
+        }
+        debug(.remoteControl, note)
+        await nightscoutManager.uploadNoteTreatment(note: note)
+    }
+}

+ 82 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Meal.swift

@@ -0,0 +1,82 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func handleMealCommand(_ pushMessage: PushMessage) async {
+        guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
+            await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let carbsDecimal = pushMessage.carbs != nil ? Decimal(pushMessage.carbs!) : nil
+        let fatDecimal = pushMessage.fat != nil ? Decimal(pushMessage.fat!) : nil
+        let proteinDecimal = pushMessage.protein != nil ? Decimal(pushMessage.protein!) : nil
+
+        let settings = await FreeAPSApp.resolver.resolve(SettingsManager.self)?.settings
+        let maxCarbs = settings?.maxCarbs ?? Decimal(0)
+        let maxFat = settings?.maxFat ?? Decimal(0)
+        let maxProtein = settings?.maxProtein ?? Decimal(0)
+
+        if let carbs = carbsDecimal, carbs > maxCarbs {
+            await logError(
+                "Command rejected: carbs amount (\(carbs)g) exceeds the maximum allowed (\(maxCarbs)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        if let fat = fatDecimal, fat > maxFat {
+            await logError(
+                "Command rejected: fat amount (\(fat)g) exceeds the maximum allowed (\(maxFat)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        if let protein = proteinDecimal, protein > maxProtein {
+            await logError(
+                "Command rejected: protein amount (\(protein)g) exceeds the maximum allowed (\(maxProtein)g).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+        let recentCarbEntries = carbsStorage.recent()
+        let carbsAfterPushMessage = recentCarbEntries.filter { $0.createdAt > pushMessageDate }
+
+        if !carbsAfterPushMessage.isEmpty {
+            await logError(
+                "Command rejected: newer carb entries have been logged since the command was sent.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        let actualDate: Date?
+        if let scheduledTime = pushMessage.scheduledTime {
+            actualDate = Date(timeIntervalSince1970: scheduledTime)
+        } else {
+            actualDate = nil
+        }
+
+        let mealEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: actualDate,
+            carbs: carbsDecimal ?? 0,
+            fat: fatDecimal,
+            protein: proteinDecimal,
+            note: "Remote meal command",
+            enteredBy: CarbsEntry.manual,
+            isFPU: false,
+            fpuID: nil
+        )
+
+        await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+}

+ 100 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift

@@ -0,0 +1,100 @@
+import Foundation
+
+extension TrioRemoteControl {
+    @MainActor internal func handleCancelOverrideCommand(_ pushMessage: PushMessage) async {
+        await disableAllActiveOverrides()
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
+        guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
+            await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+            return
+        }
+
+        let presetIDs = await overrideStorage.fetchForOverridePresets()
+
+        let presets = presetIDs.compactMap { id in
+            try? viewContext.existingObject(with: id) as? OverrideStored
+        }
+
+        if let preset = presets.first(where: { $0.name == overrideName }) {
+            await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+        } else {
+            await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
+        }
+    }
+
+    @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
+        await disableAllActiveOverrides()
+
+        preset.enabled = true
+        preset.date = Date()
+        preset.isUploadedToNS = false
+
+        do {
+            if viewContext.hasChanges {
+                try viewContext.save()
+
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                await awaitNotification(.didUpdateOverrideConfiguration)
+
+                debug(
+                    .remoteControl,
+                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+                )
+            }
+        } catch {
+            debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
+        }
+    }
+
+    @MainActor private func disableAllActiveOverrides() async {
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
+
+        let didPostNotification = await viewContext.perform { () -> Bool in
+            do {
+                let results = try ids.compactMap { id in
+                    try self.viewContext.existingObject(with: id) as? OverrideStored
+                }
+
+                guard !results.isEmpty else { return false }
+
+                for canceledOverride in results where canceledOverride.enabled {
+                    let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
+                    newOverrideRunStored.id = UUID()
+                    newOverrideRunStored.name = canceledOverride.name
+                    newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
+                    newOverrideRunStored.endDate = Date()
+                    newOverrideRunStored
+                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                    newOverrideRunStored.override = canceledOverride
+                    newOverrideRunStored.isUploadedToNS = false
+
+                    canceledOverride.enabled = false
+                }
+
+                if self.viewContext.hasChanges {
+                    try self.viewContext.save()
+                    Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                    return true
+                } else {
+                    return false
+                }
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+                )
+                return false
+            }
+        }
+
+        if didPostNotification {
+            await awaitNotification(.didUpdateOverrideConfiguration)
+        }
+    }
+}

+ 49 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -0,0 +1,49 @@
+import Foundation
+
+extension TrioRemoteControl {
+    func handleTempTargetCommand(_ pushMessage: PushMessage) async {
+        guard let targetValue = pushMessage.target,
+              let durationValue = pushMessage.duration
+        else {
+            await logError("Command rejected: temp target data is incomplete or invalid.", pushMessage: pushMessage)
+            return
+        }
+
+        let durationInMinutes = Int(durationValue)
+        let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
+
+        let tempTarget = TempTarget(
+            name: TempTarget.custom,
+            createdAt: pushMessageDate,
+            targetTop: Decimal(targetValue),
+            targetBottom: Decimal(targetValue),
+            duration: Decimal(durationInMinutes),
+            enteredBy: TempTarget.manual,
+            reason: TempTarget.custom
+        )
+
+        tempTargetsStorage.storeTempTargets([tempTarget])
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+
+    func cancelTempTarget(_ pushMessage: PushMessage) async {
+        debug(.remoteControl, "Cancelling temp target.")
+
+        guard tempTargetsStorage.current() != nil else {
+            await logError("Command rejected: no active temp target to cancel.")
+            return
+        }
+
+        let cancelEntry = TempTarget.cancel(at: Date())
+        tempTargetsStorage.storeTempTargets([cancelEntry])
+
+        debug(
+            .remoteControl,
+            "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
+        )
+    }
+}

+ 113 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift

@@ -0,0 +1,113 @@
+import CoreData
+import Foundation
+import Swinject
+
+class TrioRemoteControl: Injectable {
+    static let shared = TrioRemoteControl()
+
+    @Injected() internal var tempTargetsStorage: TempTargetsStorage!
+    @Injected() internal var carbsStorage: CarbsStorage!
+    @Injected() internal var nightscoutManager: NightscoutManager!
+    @Injected() internal var overrideStorage: OverrideStorage!
+    @Injected() internal var settings: SettingsManager!
+
+    private let timeWindow: TimeInterval = 600 // Defines how old messages that are accepted, 10 minutes
+
+    internal let pumpHistoryFetchContext: NSManagedObjectContext
+    internal let viewContext: NSManagedObjectContext
+
+    private init() {
+        pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
+        viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        injectServices(FreeAPSApp.resolver)
+    }
+
+    func handleRemoteNotification(pushMessage: PushMessage) async {
+        let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
+        guard isTrioRemoteControlEnabled else {
+            await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
+            return
+        }
+
+        let currentTime = Date().timeIntervalSince1970
+        let timeDifference = currentTime - pushMessage.timestamp
+
+        if timeDifference > timeWindow {
+            await logError(
+                "Command rejected: the message is too old (sent \(Int(timeDifference)) seconds ago, which exceeds the allowed limit).",
+                pushMessage: pushMessage
+            )
+            return
+        } else if timeDifference < -timeWindow {
+            await logError(
+                "Command rejected: the message has an invalid future timestamp (timestamp is \(Int(-timeDifference)) seconds ahead of the current time).",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        debug(.remoteControl, "Command received with acceptable time difference: \(Int(timeDifference)) seconds.")
+
+        let storedSecret = UserDefaults.standard.string(forKey: "trioRemoteControlSharedSecret") ?? ""
+        guard !storedSecret.isEmpty else {
+            await logError(
+                "Command rejected: shared secret is missing in settings. Cannot authenticate the command.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        guard pushMessage.sharedSecret == storedSecret else {
+            await logError(
+                "Command rejected: shared secret does not match. Cannot authenticate the command.",
+                pushMessage: pushMessage
+            )
+            return
+        }
+
+        switch pushMessage.commandType {
+        case .bolus:
+            await handleBolusCommand(pushMessage)
+        case .tempTarget:
+            await handleTempTargetCommand(pushMessage)
+        case .cancelTempTarget:
+            await cancelTempTarget(pushMessage)
+        case .meal:
+            await handleMealCommand(pushMessage)
+        case .startOverride:
+            await handleStartOverrideCommand(pushMessage)
+        case .cancelOverride:
+            await handleCancelOverrideCommand(pushMessage)
+        }
+    }
+}
+
+// MARK: - CommandType Enum
+
+extension TrioRemoteControl {
+    enum CommandType: String, Codable {
+        case bolus
+        case tempTarget = "temp_target"
+        case cancelTempTarget = "cancel_temp_target"
+        case meal
+        case startOverride = "start_override"
+        case cancelOverride = "cancel_override"
+
+        var description: String {
+            switch self {
+            case .bolus:
+                return "Bolus"
+            case .tempTarget:
+                return "Temporary Target"
+            case .cancelTempTarget:
+                return "Cancel Temporary Target"
+            case .meal:
+                return "Meal"
+            case .startOverride:
+                return "Start Override"
+            case .cancelOverride:
+                return "Cancel Override"
+            }
+        }
+    }
+}

+ 6 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigDataFlow.swift

@@ -0,0 +1,6 @@
+import Foundation
+enum RemoteControlConfig {
+    enum Config {}
+}
+
+protocol RemoteControlConfigProvider {}

+ 4 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigProvider.swift

@@ -0,0 +1,4 @@
+import Foundation
+extension RemoteControlConfig {
+    final class Provider: BaseProvider, RemoteControlConfigProvider {}
+}

+ 0 - 0
FreeAPS/Sources/Modules/RemoteControlConfig/RemoteControlConfigStateModel.swift


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů