Jelajahi Sumber

Merge branch 'nightscout:dev' into ReclassifyInfoErrorMessages2

kskandis 1 tahun lalu
induk
melakukan
b0fcc175dd
78 mengubah file dengan 32641 tambahan dan 8220 penghapusan
  1. 31 1
      .github/workflows/build_trio.yml
  2. 1 1
      DanaKit
  3. 1 1
      MinimedKit
  4. 14 0
      Model/Helper/Determination+helper.swift
  5. 1 1
      Model/Helper/PumpEvent+helper.swift
  6. 152 24
      Trio.xcodeproj/project.pbxproj
  7. 30 172
      Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  8. 2 2
      Trio/Resources/json/defaults/freeaps/freeaps_settings.json
  9. 13 165
      Trio/Sources/APS/APSManager.swift
  10. 2 2
      Trio/Sources/APS/FetchGlucoseManager.swift
  11. 17 6
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  12. 36 258
      Trio/Sources/APS/PluginManager.swift
  13. 141 113
      Trio/Sources/APS/Storage/TDDStorage.swift
  14. 1 0
      Trio/Sources/Application/AppDelegate.swift
  15. 1 2
      Trio/Sources/Application/TrioApp.swift
  16. 9 3
      Trio/Sources/Helpers/BuildDetails.swift
  17. 30034 6162
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  18. 2 2
      Trio/Sources/Models/DecimalPickerSettings.swift
  19. 6 0
      Trio/Sources/Models/Determination.swift
  20. 2 1
      Trio/Sources/Models/Preferences.swift
  21. 0 16
      Trio/Sources/Models/TimeInRangeChartStyle.swift
  22. 34 0
      Trio/Sources/Models/TimeInRangeType.swift
  23. 10 5
      Trio/Sources/Models/TrioSettings.swift
  24. 3 0
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  25. 3 0
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  26. 5 1
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  27. 4 0
      Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  28. 52 0
      Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  29. 5 18
      Trio/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift
  30. 33 2
      Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  31. 60 47
      Trio/Sources/Modules/CGMSettings/CGMSettingsStateModel.swift
  32. 6 3
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  33. 4 6
      Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift
  34. 17 6
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  35. 118 6
      Trio/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift
  36. 126 179
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  37. 2 0
      Trio/Sources/Modules/GeneralSettings/UnitsLimitsSettingsStateModel.swift
  38. 48 1
      Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  39. 79 0
      Trio/Sources/Modules/Home/HomeStateModel+CGM.swift
  40. 1 2
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  41. 29 64
      Trio/Sources/Modules/Home/HomeStateModel.swift
  42. 16 5
      Trio/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift
  43. 33 5
      Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift
  44. 32 23
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  45. 6 6
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  46. 0 4
      Trio/Sources/Modules/SMBSettings/SMBSettingsStateModel.swift
  47. 2 54
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  48. 41 47
      Trio/Sources/Modules/Settings/SettingItems.swift
  49. 7 1
      Trio/Sources/Modules/Settings/SettingsStateModel.swift
  50. 17 5
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  51. 4 1
      Trio/Sources/Modules/Settings/View/Subviews/SubmodulesView.swift
  52. 25 13
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  53. 6 3
      Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift
  54. 3 9
      Trio/Sources/Modules/Stat/StatStateModel.swift
  55. 4 2
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  56. 10 6
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  57. 20 12
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift
  58. 14 5
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  59. 2 8
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  60. 6 1
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift
  61. 0 58
      Trio/Sources/Modules/StatConfig/StatConfigStateModel.swift
  62. 0 113
      Trio/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  63. 6 0
      Trio/Sources/Modules/Treatments/TreatmentsProvider.swift
  64. 40 5
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  65. 16 10
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  66. 870 379
      Trio/Sources/Modules/Treatments/View/PopupView.swift
  67. 67 19
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  68. 2 2
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  69. 52 13
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  70. 87 22
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  71. 70 10
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  72. 4 3
      Trio/Sources/Services/Network/TidepoolManager.swift
  73. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  74. 12 3
      Trio/Sources/Views/SettingInputSection.swift
  75. 29 5
      TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift
  76. 0 53
      TrioTests/PluginManagerTests.swift
  77. 1 0
      patches/save_patches_here.md
  78. 0 41
      scripts/copy-plugins.sh

+ 31 - 1
.github/workflows/build_trio.yml

@@ -201,7 +201,7 @@ jobs:
       )
     steps:
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_16.0.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_16.2.app/Contents/Developer"
 
       - name: Checkout Repo for syncing
         if: |
@@ -253,6 +253,36 @@ jobs:
           submodules: recursive
           ref: ${{ env.TARGET_BRANCH }}
 
+      # Customize Trio: Use patches or download and apply patches from GitHub
+      - name: Customize Trio
+        run: |
+
+          # Trio workspace patches
+          # -applies any patches located in the Trio/patches/ directory
+          if $(ls ./patches/* &> /dev/null); then
+          git apply ./patches/* --allow-empty -v --whitespace=fix
+          fi
+
+          # Download and apply Trio patches from GitHub:
+          # Template for customizing Trio code (as opposed to submodule code)
+          # Remove the "#" sign from the beginning of the line below to activate
+          #   and then replace the alphanumeric string with your SHA, this SHA is NOT valid
+          #curl https://github.com/nightscout/Trio/commit/d206432b024279ef710df462b20bd464cd9682d4.patch | git apply -v --whitespace=fix
+
+          # Download and apply Submodule patches from GitHub:
+          # Template for customizing submodules (you must edit the submodule name)
+          # This example is for G7SensorKit showing you can apply multiple commits, in the proper order
+          # Remove the "#" sign from the beginning of the lines below to activate
+          # This example applies 3 commits from the scan-fix folder; valid only when these are not already in Trio
+          #curl https://github.com/loopandlearn/G7SensorKit/commit/ba44beb3d1491c453f4f438443c3f8ba29146ab3.patch | git apply --directory=G7SensorKit -v --whitespace=fix
+          #curl https://github.com/loopandlearn/G7SensorKit/commit/d86ac8e9cd523d1267587dd70c96597125eef7ab.patch | git apply --directory=G7SensorKit -v --whitespace=fix
+          #curl https://github.com/loopandlearn/G7SensorKit/commit/205054e7537723c2aec58d807634b4853f687244.patch | git apply --directory=G7SensorKit -v --whitespace=fix
+
+          # Add patches for additional customization by following the templates above,
+          # and make sure to specify the submodule by setting "--directory=(submodule_name)".
+          # Several patches may be added per submodule.
+          # Adding comments (#) is strongly recommended to easily tell the individual patches apart.
+
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
         run: |

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit b44c5df260a8b38d6fd0b5851cc3aac5da5d9d57
+Subproject commit 200937c3c985de4cb05604cf1d7af2a307dfcaf3

+ 1 - 1
MinimedKit

@@ -1 +1 @@
-Subproject commit f11abde5e2eea2cbf7ac80f3f4bc4bc6e7f6de56
+Subproject commit ecd3588bdab3844617e17601ba2da1c28e786e77

+ 14 - 0
Model/Helper/Determination+helper.swift

@@ -19,6 +19,20 @@ extension OrefDetermination {
     var reasonConclusion: String {
         reason?.components(separatedBy: "; ").last ?? ""
     }
+
+    var minPredBGFromReason: Decimal? {
+        // Find the part that contains "minPredBG"
+        if let minPredBGPart = reasonParts.first(where: { $0.contains("minPredBG") }) {
+            // Extract the number after "minPredBG"
+            let components = minPredBGPart.components(separatedBy: "minPredBG ")
+            if let valueComponent = components.dropFirst().first {
+                // Get everything after "minPredBG " and convert to Decimal
+                let valueString = valueComponent.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.-").inverted)
+                return Decimal(string: valueString)
+            }
+        }
+        return nil
+    }
 }
 
 extension NSPredicate {

+ 1 - 1
Model/Helper/PumpEvent+helper.swift

@@ -97,7 +97,7 @@ extension NSPredicate {
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         return NSPredicate(
-            format: "type == %@ AND timestamp <= %@",
+            format: "type == %@ AND timestamp >= %@",
             PumpEventStored.EventType.tempBasal.rawValue,
             date as NSDate
         )

+ 152 - 24
Trio.xcodeproj/project.pbxproj

@@ -204,8 +204,49 @@
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
+		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
+		3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; };
+		3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; };
+		3B4BA76D2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA76E2D8DBD690069D5B8 /* DanaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */; };
+		3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7702D8DBD690069D5B8 /* G7SensorKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */; };
+		3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7722D8DBD690069D5B8 /* G7SensorKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */; };
+		3B4BA7732D8DBD690069D5B8 /* G7SensorKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7742D8DBD690069D5B8 /* LibreTransmitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */; };
+		3B4BA7752D8DBD690069D5B8 /* LibreTransmitter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */; };
+		3B4BA7772D8DBD690069D5B8 /* LibreTransmitterUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */; };
+		3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA77A2D8DBD690069D5B8 /* MinimedKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */; };
+		3B4BA77B2D8DBD690069D5B8 /* MinimedKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */; };
+		3B4BA77D2D8DBD690069D5B8 /* OmniBLE.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA77E2D8DBD690069D5B8 /* OmniKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */; };
+		3B4BA77F2D8DBD690069D5B8 /* OmniKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */; };
+		3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7822D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */; };
+		3B4BA7832D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7842D8DBD690069D5B8 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */; };
+		3B4BA7852D8DBD690069D5B8 /* RileyLinkKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */; };
+		3B4BA7872D8DBD690069D5B8 /* RileyLinkKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA78A2D8DC0EC0069D5B8 /* ShareClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; };
+		3B4BA78B2D8DC0EC0069D5B8 /* ShareClient.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE398D1A297D69A900DF218F /* ShareClient.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA78C2D8DC0EC0069D5B8 /* ShareClientUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE79502D29980E4D00FA576E /* ShareClientUI.framework */; };
+		3B4BA78D2D8DC0EC0069D5B8 /* ShareClientUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE79502D29980E4D00FA576E /* ShareClientUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA78E2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */; };
+		3B4BA78F2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; };
+		3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
+		3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; };
+		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
@@ -448,7 +489,6 @@
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
-		DD07CA142CE80B73002D45A9 /* TimeInRangeChartStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */; };
 		DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */; };
 		DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */; };
 		DD09D47F2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */; };
@@ -580,6 +620,7 @@
 		DDE1796F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift */; };
 		DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */; };
 		DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */; };
+		DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */; };
 		DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */; };
 		DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */; };
 		DDF847E12C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */; };
@@ -667,10 +708,29 @@
 			dstSubfolderSpec = 10;
 			files = (
 				CE51DD1D2A01970900F163F7 /* ConnectIQ 2.xcframework in Embed Frameworks */,
+				3B4BA78F2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Embed Frameworks */,
+				3B4BA7732D8DBD690069D5B8 /* G7SensorKitUI.framework in Embed Frameworks */,
+				3B4BA76D2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Embed Frameworks */,
+				3B4BA76B2D8DBD690069D5B8 /* CGMBLEKit.framework in Embed Frameworks */,
+				3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */,
 				CE95BF5C2BA770C300DC3DE3 /* LoopKit.framework in Embed Frameworks */,
+				3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */,
 				CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */,
+				3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */,
+				3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */,
+				3B4BA77D2D8DBD690069D5B8 /* OmniBLE.framework in Embed Frameworks */,
+				3B4BA77F2D8DBD690069D5B8 /* OmniKit.framework in Embed Frameworks */,
+				3B4BA7852D8DBD690069D5B8 /* RileyLinkKit.framework in Embed Frameworks */,
+				3B4BA7752D8DBD690069D5B8 /* LibreTransmitter.framework in Embed Frameworks */,
+				3B4BA7772D8DBD690069D5B8 /* LibreTransmitterUI.framework in Embed Frameworks */,
+				3B4BA77B2D8DBD690069D5B8 /* MinimedKitUI.framework in Embed Frameworks */,
+				3B4BA7832D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Embed Frameworks */,
 				CE95BF642BA771BE00DC3DE3 /* LoopTestingKit.framework in Embed Frameworks */,
 				CE95BF622BA7715900DC3DE3 /* MockKitUI.framework in Embed Frameworks */,
+				3B4BA78D2D8DC0EC0069D5B8 /* ShareClientUI.framework in Embed Frameworks */,
+				3B4BA7872D8DBD690069D5B8 /* RileyLinkKitUI.framework in Embed Frameworks */,
+				3B4BA78B2D8DC0EC0069D5B8 /* ShareClient.framework in Embed Frameworks */,
+				3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */,
 				CE95BF602BA7715800DC3DE3 /* MockKit.framework in Embed Frameworks */,
 				CE95BF5E2BA770C300DC3DE3 /* LoopKitUI.framework in Embed Frameworks */,
 			);
@@ -925,6 +985,24 @@
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
+		3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DanaKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LibreTransmitter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LibreTransmitterUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MinimedKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
@@ -1175,7 +1253,6 @@
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsStateModel.swift; sourceTree = "<group>"; };
-		DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeChartStyle.swift; sourceTree = "<group>"; };
 		DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsDataFlow.swift; sourceTree = "<group>"; };
 		DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsProvider.swift; sourceTree = "<group>"; };
 		DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1309,6 +1386,7 @@
 		DDE1794F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrefDetermination+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeType.swift; sourceTree = "<group>"; };
 		DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsDataFlow.swift; sourceTree = "<group>"; };
 		DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsProvider.swift; sourceTree = "<group>"; };
 		DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1357,17 +1435,38 @@
 			buildActionMask = 2147483647;
 			files = (
 				CE95BF632BA771BE00DC3DE3 /* LoopTestingKit.framework in Frameworks */,
+				3B4BA7722D8DBD690069D5B8 /* G7SensorKitUI.framework in Frameworks */,
+				3B4BA7742D8DBD690069D5B8 /* LibreTransmitter.framework in Frameworks */,
+				3B4BA78C2D8DC0EC0069D5B8 /* ShareClientUI.framework in Frameworks */,
+				3B4BA77A2D8DBD690069D5B8 /* MinimedKitUI.framework in Frameworks */,
+				3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */,
+				3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */,
+				3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */,
+				3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */,
+				3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */,
+				3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */,
 				38E87403274F78C000975559 /* libswiftCoreNFC.tbd in Frameworks */,
 				38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */,
+				3B4BA78A2D8DC0EC0069D5B8 /* ShareClient.framework in Frameworks */,
+				3B4BA77E2D8DBD690069D5B8 /* OmniKit.framework in Frameworks */,
 				CE51DD1C2A01970900F163F7 /* ConnectIQ 2.xcframework in Frameworks */,
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
+				3B4BA78E2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Frameworks */,
 				B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */,
+				3B4BA7702D8DBD690069D5B8 /* G7SensorKit.framework in Frameworks */,
+				3B4BA76C2D8DBD690069D5B8 /* CGMBLEKitUI.framework in Frameworks */,
 				CE95BF5B2BA770C300DC3DE3 /* LoopKit.framework in Frameworks */,
 				38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */,
 				3833B46D26012030003021B3 /* Algorithms in Frameworks */,
+				3B4BA7822D8DBD690069D5B8 /* RileyLinkBLEKit.framework in Frameworks */,
+				3B4BA76E2D8DBD690069D5B8 /* DanaKit.framework in Frameworks */,
+				3B4BA7862D8DBD690069D5B8 /* RileyLinkKitUI.framework in Frameworks */,
 				CEB434FD28B90B7C00B70274 /* SwiftCharts in Frameworks */,
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
+				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
 				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
+				3B4BA7802D8DBD690069D5B8 /* OmniKitUI.framework in Frameworks */,
+				3B4BA7842D8DBD690069D5B8 /* RileyLinkKit.framework in Frameworks */,
 				CE95BF612BA7715900DC3DE3 /* MockKitUI.framework in Frameworks */,
 				E0CC2C5C275B9F0F00A7BC71 /* HealthKit.framework in Frameworks */,
 				CE95BF5D2BA770C300DC3DE3 /* LoopKitUI.framework in Frameworks */,
@@ -1755,6 +1854,7 @@
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
+				3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */,
 				58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */,
 				3811DE2C25C9D49500A708ED /* View */,
 			);
@@ -1938,6 +2038,23 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,
+				3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */,
+				3B4BA75B2D8DBD690069D5B8 /* CGMBLEKit.framework */,
+				3B4BA75C2D8DBD690069D5B8 /* CGMBLEKitUI.framework */,
+				3B4BA75D2D8DBD690069D5B8 /* DanaKit.framework */,
+				3B4BA75E2D8DBD690069D5B8 /* G7SensorKit.framework */,
+				3B4BA75F2D8DBD690069D5B8 /* G7SensorKitUI.framework */,
+				3B4BA7602D8DBD690069D5B8 /* LibreTransmitter.framework */,
+				3B4BA7612D8DBD690069D5B8 /* LibreTransmitterUI.framework */,
+				3B4BA7622D8DBD690069D5B8 /* MinimedKit.framework */,
+				3B4BA7632D8DBD690069D5B8 /* MinimedKitUI.framework */,
+				3B4BA7642D8DBD690069D5B8 /* OmniBLE.framework */,
+				3B4BA7652D8DBD690069D5B8 /* OmniKit.framework */,
+				3B4BA7662D8DBD690069D5B8 /* OmniKitUI.framework */,
+				3B4BA7672D8DBD690069D5B8 /* RileyLinkBLEKit.framework */,
+				3B4BA7682D8DBD690069D5B8 /* RileyLinkKit.framework */,
+				3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */,
 				CE95BF492BA5CED700DC3DE3 /* LoopKit.framework */,
 				CE95BF4A2BA5CED700DC3DE3 /* LoopKitUI.framework */,
 				CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */,
@@ -2097,6 +2214,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
 				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
 				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
@@ -2106,7 +2224,6 @@
 				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
 				BDA25EFC2D261BF200035F34 /* WatchState.swift */,
 				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
-				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
@@ -3349,7 +3466,6 @@
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
 				38E8753D27554D5900975559 /* Embed Watch Content */,
 				6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */,
-				CE95BF582BA5F8F300DC3DE3 /* Install plugins */,
 				DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */,
 			);
 			buildRules = (
@@ -3366,6 +3482,8 @@
 				38DF1788276FC8C400B3528F /* SwiftMessages */,
 				CEB434FC28B90B7C00B70274 /* SwiftCharts */,
 				B958F1B62BA0711600484851 /* MKRingProgressView */,
+				3BD9687B2D8DDD4600899469 /* SlideButton */,
+				3BD9687E2D8DDD8800899469 /* CryptoSwift */,
 			);
 			productName = Trio;
 			productReference = 388E595825AD948C0019842D /* Trio.app */;
@@ -3539,6 +3657,8 @@
 				38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */,
 				CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */,
 				B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */,
+				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
+				3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -3633,25 +3753,6 @@
 			shellPath = /bin/sh;
 			shellScript = "source \"${SRCROOT}\"/scripts/swiftformat.sh\n\n";
 		};
-		CE95BF582BA5F8F300DC3DE3 /* Install plugins */ = {
-			isa = PBXShellScriptBuildPhase;
-			alwaysOutOfDate = 1;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputFileListPaths = (
-			);
-			inputPaths = (
-			);
-			name = "Install plugins";
-			outputFileListPaths = (
-			);
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "\"${SRCROOT}/Scripts/copy-plugins.sh\"\n";
-		};
 		DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */ = {
 			isa = PBXShellScriptBuildPhase;
 			alwaysOutOfDate = 1;
@@ -3710,7 +3811,6 @@
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
-				DD07CA142CE80B73002D45A9 /* TimeInRangeChartStyle.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
@@ -3880,6 +3980,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
+				3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,
@@ -4062,6 +4163,7 @@
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
 				58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,
+				DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */,
 				DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
@@ -4935,6 +5037,22 @@
 				minimumVersion = 9.0.0;
 			};
 		};
+		3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/no-comment/SlideButton";
+			requirement = {
+				branch = main;
+				kind = branch;
+			};
+		};
+		3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.8.2;
+			};
+		};
 		B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/maxkonovalov/MKRingProgressView.git";
@@ -4974,6 +5092,16 @@
 			package = 38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */;
 			productName = SwiftMessages;
 		};
+		3BD9687B2D8DDD4600899469 /* SlideButton */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */;
+			productName = SlideButton;
+		};
+		3BD9687E2D8DDD8800899469 /* CryptoSwift */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 3BD9687D2D8DDD8800899469 /* XCRemoteSwiftPackageReference "CryptoSwift" */;
+			productName = CryptoSwift;
+		};
 		B958F1B62BA0711600484851 /* MKRingProgressView */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */;

+ 30 - 172
Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -98,166 +98,54 @@
             buildForAnalyzing = "YES">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C1E34B5A29C7AD01009A50A5"
-               BuildableName = "MinimedKitPlugin.loopplugin"
-               BlueprintName = "MinimedKitPlugin"
-               ReferencedContainer = "container:MinimedKit/MinimedKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C124021629C7D93D00B32844"
-               BuildableName = "OmniKitPlugin.loopplugin"
-               BlueprintName = "OmniKitPlugin"
-               ReferencedContainer = "container:OmniKit/OmniKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B4D40D3D23A428BC00D7ECB5"
-               BuildableName = "CGMBLEKitG5Plugin.loopplugin"
-               BlueprintName = "CGMBLEKitG5Plugin"
-               ReferencedContainer = "container:CGMBLEKit/CGMBLEKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B4D40D2D23A3E91800D7ECB5"
-               BuildableName = "CGMBLEKitG6Plugin.loopplugin"
-               BlueprintName = "CGMBLEKitG6Plugin"
-               ReferencedContainer = "container:CGMBLEKit/CGMBLEKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B40BF25D23ABD47400A43CEE"
-               BuildableName = "ShareClientPlugin.loopplugin"
-               BlueprintName = "ShareClientPlugin"
-               ReferencedContainer = "container:dexcom-share-client-swift/ShareClient.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "3E6007862D0C5D0C00B186D1"
-               BuildableName = "DanaKitPlugin.loopplugin"
-               BlueprintName = "DanaKitPlugin"
-               ReferencedContainer = "container:DanaKit/DanaKit.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C17F511C291EACCD00555EB5"
-               BuildableName = "G7SensorPlugin.loopplugin"
-               BlueprintName = "G7SensorPlugin"
-               ReferencedContainer = "container:G7SensorKit/G7SensorKit.xcodeproj">
+               BlueprintIdentifier = "388E595725AD948C0019842D"
+               BuildableName = "Trio.app"
+               BlueprintName = "Trio"
+               ReferencedContainer = "container:Trio.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
          <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
+            buildForTesting = "NO"
+            buildForRunning = "NO"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C1BDBAE72A4397E200A787D1"
-               BuildableName = "LibreDemoPlugin.loopplugin"
-               BlueprintName = "LibreDemoPlugin"
-               ReferencedContainer = "container:LibreTransmitter/LibreTransmitter.xcodeproj">
+               BlueprintIdentifier = "38FCF3EC25E9028E0078B0D1"
+               BuildableName = "TrioTests.xctest"
+               BlueprintName = "TrioTests"
+               ReferencedContainer = "container:Trio.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
          <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
+            buildForTesting = "NO"
+            buildForRunning = "NO"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "B40BF25D23ABD47400A43CEE"
-               BuildableName = "LibreTransmitterPlugin.loopplugin"
-               BlueprintName = "LibreTransmitterPlugin"
-               ReferencedContainer = "container:LibreTransmitter/LibreTransmitter.xcodeproj">
+               BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
+               BuildableName = "LoopKitTests.xctest"
+               BlueprintName = "LoopKitTests"
+               ReferencedContainer = "container:LoopKit/LoopKit.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
          <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
+            buildForTesting = "NO"
+            buildForRunning = "NO"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "C187C196279086A8006E3557"
-               BuildableName = "OmniBLEPlugin.loopplugin"
-               BlueprintName = "OmniBLEPlugin"
+               BlueprintIdentifier = "84752E8A26ED0FFE009FD801"
+               BuildableName = "OmniBLETests.xctest"
+               BlueprintName = "OmniBLETests"
                ReferencedContainer = "container:OmniBLE/OmniBLE.xcodeproj">
             </BuildableReference>
          </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "A94AE4E3235A89B5005CA320"
-               BuildableName = "TidepoolServiceKitPlugin.loopplugin"
-               BlueprintName = "TidepoolServiceKitPlugin"
-               ReferencedContainer = "container:TidepoolService/TidepoolService.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "388E595725AD948C0019842D"
-               BuildableName = "Trio.app"
-               BlueprintName = "Trio"
-               ReferencedContainer = "container:Trio.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
       </BuildActionEntries>
    </BuildAction>
    <TestAction
@@ -270,16 +158,6 @@
             skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "38FCF3EC25E9028E0078B0D1"
-               BuildableName = "TrioTests.xctest"
-               BlueprintName = "TrioTests"
-               ReferencedContainer = "container:Trio.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
                BlueprintIdentifier = "43CABDFC1C3506F100005705"
                BuildableName = "CGMBLEKitTests.xctest"
                BlueprintName = "CGMBLEKitTests"
@@ -300,16 +178,6 @@
             skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "43D8FDD41C728FDF0073BE78"
-               BuildableName = "LoopKitTests.xctest"
-               BlueprintName = "LoopKitTests"
-               ReferencedContainer = "container:LoopKit/LoopKit.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
                BlueprintIdentifier = "B4CEE2DF257129780093111B"
                BuildableName = "MockKitTests.xctest"
                BlueprintName = "MockKitTests"
@@ -330,16 +198,6 @@
             skipped = "NO">
             <BuildableReference
                BuildableIdentifier = "primary"
-               BlueprintIdentifier = "84752E8A26ED0FFE009FD801"
-               BuildableName = "OmniBLETests.xctest"
-               BlueprintName = "OmniBLETests"
-               ReferencedContainer = "container:OmniBLE/OmniBLE.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
                BlueprintIdentifier = "C12ED9C929C7DBA900435701"
                BuildableName = "OmniKitTests.xctest"
                BlueprintName = "OmniKitTests"

+ 2 - 2
Trio/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -33,7 +33,6 @@
   "glucoseColorScheme" : "staticColor",
   "xGridLines" : true,
   "yGridLines" : true,
-  "timeInRangeChartStyle" : "vertical",
   "rulerMarks" : true,
   "forecastDisplayType": "cone",
   "maxCarbs": 250,
@@ -48,5 +47,6 @@
   "lockScreenView": "simple",
   "useCalendar": false,
   "displayCalendarIOBandCOB": false,
-  "displayCalendarEmojis": false
+  "displayCalendarEmojis": false,
+  "timeInRangeType": "timeInTightRange"
 }

+ 13 - 165
Trio/Sources/APS/APSManager.swift

@@ -33,23 +33,20 @@ enum APSError: LocalizedError {
     case invalidPumpState(message: String)
     case glucoseError(message: String)
     case apsError(message: String)
-    case deviceSyncError(message: String)
     case manualBasalTemp(message: String)
 
     var errorDescription: String? {
         switch self {
         case let .pumpError(error):
-            return "Pump error: \(error.localizedDescription)"
+            return String(localized: "Pump Error (\(error.localizedDescription)).")
         case let .invalidPumpState(message):
-            return "Error: Invalid Pump State: \(message)"
+            return String(localized: "Invalid Pump State (\(message)).")
         case let .glucoseError(message):
-            return "Error: Invalid glucose: \(message)"
+            return String(localized: "Invalid Glucose (\(message)).")
         case let .apsError(message):
-            return "APS error: \(message)"
-        case let .deviceSyncError(message):
-            return "Sync error: \(message)"
+            return String(localized: "Invalid Algorithm Response (\(message)).")
         case let .manualBasalTemp(message):
-            return "Manual Basal Temp : \(message)"
+            return String(localized: "Manual Temporary Basal Rate (\(message)). Looping suspended.")
         }
     }
 
@@ -122,7 +119,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     init(resolver: Resolver) {
         injectServices(resolver)
-        openAPS = OpenAPS(storage: storage)
+        openAPS = OpenAPS(storage: storage, tddStorage: tddStorage)
         subscribe()
         lastLoopDateSubject.send(lastLoopDate)
 
@@ -356,21 +353,21 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private func verifyStatus() -> Error? {
         guard let pump = pumpManager else {
-            return APSError.invalidPumpState(message: "Pump not set")
+            return APSError.invalidPumpState(message: String(localized: "Pump not set"))
         }
         let status = pump.status.pumpStatus
 
         guard !status.bolusing else {
-            return APSError.invalidPumpState(message: "Pump is bolusing")
+            return APSError.invalidPumpState(message: String(localized: "Pump is bolusing"))
         }
 
         guard !status.suspended else {
-            return APSError.invalidPumpState(message: "Pump suspended")
+            return APSError.invalidPumpState(message: String(localized: "Pump suspended"))
         }
 
         let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
         guard reservoir >= 0 else {
-            return APSError.invalidPumpState(message: "Reservoir is empty")
+            return APSError.invalidPumpState(message: String(localized: "Reservoir is empty"))
         }
 
         return nil
@@ -422,20 +419,20 @@ final class BaseAPSManager: APSManager, Injectable {
 
             guard glucose.count > 2 else {
                 debug(.apsManager, "Not enough glucose data")
-                self.processError(APSError.glucoseError(message: "Not enough glucose data"))
+                self.processError(APSError.glucoseError(message: String(localized: "Not enough glucose data")))
                 return false
             }
 
             let dateOfLastGlucose = glucose.first?.date
             guard dateOfLastGlucose ?? Date() >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
                 debug(.apsManager, "Glucose data is stale")
-                self.processError(APSError.glucoseError(message: "Glucose data is stale"))
+                self.processError(APSError.glucoseError(message: String(localized: "Glucose data is stale")))
                 return false
             }
 
             guard !GlucoseStored.glucoseIsFlat(glucose) else {
                 debug(.apsManager, "Glucose data is too flat")
-                self.processError(APSError.glucoseError(message: "Glucose data is too flat"))
+                self.processError(APSError.glucoseError(message: String(localized: "Glucose data is too flat")))
                 return false
             }
 
@@ -720,10 +717,6 @@ final class BaseAPSManager: APSManager, Injectable {
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
-
-                Task.detached(priority: .low) {
-                    await self.statistics()
-                }
             }
         } catch {
             debug(
@@ -909,125 +902,6 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    // TODO: - Refactor this whole shit here...
-
-    // Add to statistics.JSON for upload to NS.
-    private func statistics() async {
-        let now = Date()
-        if settingsManager.settings.uploadStats != nil {
-            let hour = Calendar.current.component(.hour, from: now)
-            guard hour > 20 else {
-                return
-            }
-
-            // MARK: - Core Data related
-
-            async let glucoseStats = glucoseForStats()
-            async let lastLoopForStats = lastLoopForStats()
-            async let carbTotal = carbsForStats()
-            async let preferences = settingsManager.preferences
-
-            let loopStats = await loopStats(oneDayGlucose: Double(rawValue: (await glucoseStats?.oneDayGlucose.readings)!) ?? 0.0)
-
-            // Only save and upload once per day
-            guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
-
-            let units = settingsManager.settings.units
-
-            // MARK: - Not Core Data related stuff
-
-            let pref = await preferences
-            var algo_ = "Oref0"
-
-            if pref.sigmoid, pref.enableDynamicCR {
-                algo_ = "Dynamic ISF + CR: Sigmoid"
-            } else if pref.sigmoid, !pref.enableDynamicCR {
-                algo_ = "Dynamic ISF: Sigmoid"
-            } else if pref.useNewFormula, pref.enableDynamicCR {
-                algo_ = "Dynamic ISF + CR: Logarithmic"
-            } else if pref.useNewFormula, !pref.sigmoid,!pref.enableDynamicCR {
-                algo_ = "Dynamic ISF: Logarithmic"
-            }
-            let af = pref.adjustmentFactor
-            let insulin_type = pref.curve
-            let buildDate = BuildDetails.shared.buildDate()
-            let version = Bundle.main.releaseVersionNumber
-            let build = Bundle.main.buildVersionNumber
-
-            var branch = BuildDetails.shared.branchAndSha
-
-            let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
-            let pump_ = pumpManager?.localizedTitle ?? ""
-            let cgm = settingsManager.settings.cgm
-            let file = OpenAPS.Monitor.statistics
-            var iPa: Decimal = 75
-            if pref.useCustomPeakTime {
-                iPa = pref.insulinPeakTime
-            } else if pref.curve.rawValue == "rapid-acting" {
-                iPa = 65
-            } else if pref.curve.rawValue == "ultra-rapid" {
-                iPa = 50
-            }
-
-            // Insulin placeholder
-            let insulin = Ins(
-                TDD: 0,
-                bolus: 0,
-                temp_basal: 0,
-                scheduled_basal: 0,
-                total_average: 0
-            )
-            guard let processedGlucoseStats = await glucoseStats else { return }
-
-            let eA1cDisplayUnit = processedGlucoseStats.eA1cDisplayUnit
-
-            let dailystat = await Statistics(
-                created_at: Date(),
-                iPhone: UIDevice.current.getDeviceId,
-                iOS: UIDevice.current.getOSInfo,
-                Build_Version: version ?? "",
-                Build_Number: build ?? "1",
-                Branch: branch,
-                CopyRightNotice: String(copyrightNotice_.prefix(32)),
-                Build_Date: buildDate ?? Date(),
-                Algorithm: algo_,
-                AdjustmentFactor: af,
-                Pump: pump_,
-                CGM: cgm.rawValue,
-                insulinType: insulin_type.rawValue,
-                peakActivityTime: iPa,
-                Carbs_24h: await carbTotal,
-                GlucoseStorage_Days: Decimal(roundDouble(Double(rawValue: processedGlucoseStats.numberofDays) ?? 0.0, 1)),
-                Statistics: Stats(
-                    Distribution: processedGlucoseStats.TimeInRange,
-                    Glucose: processedGlucoseStats.avg,
-                    EstimatedA1c: processedGlucoseStats.hbs,
-                    Units: Units(Glucose: units.rawValue, EstimatedA1c: eA1cDisplayUnit.rawValue),
-                    LoopCycles: loopStats,
-                    Insulin: insulin,
-                    Variance: processedGlucoseStats.variance
-                )
-            )
-            storage.save(dailystat, as: file)
-
-            await saveStatsToCoreData()
-        }
-    }
-
-    private func saveStatsToCoreData() async {
-        await privateContext.perform {
-            let saveStatsCoreData = StatsData(context: self.privateContext)
-            saveStatsCoreData.lastrun = Date()
-
-            do {
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
-            } catch {
-                print(error.localizedDescription)
-            }
-        }
-    }
-
     private func lastLoopForStats() async -> Date? {
         let requestStats = StatsData.fetchRequest() as NSFetchRequest<StatsData>
         let sortStats = NSSortDescriptor(key: "lastrun", ascending: false)
@@ -1044,32 +918,6 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func carbsForStats() async -> Decimal {
-        let requestCarbs = CarbEntryStored.fetchRequest() as NSFetchRequest<CarbEntryStored>
-        let daysAgo = Date().addingTimeInterval(-1.days.timeInterval)
-        requestCarbs.predicate = NSPredicate(format: "carbs > 0 AND date > %@", daysAgo as NSDate)
-        requestCarbs.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
-
-        return await privateContext.perform {
-            do {
-                let carbs = try self.privateContext.fetch(requestCarbs)
-                debugPrint(
-                    "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.succeeded) fetched carbs"
-                )
-
-                return carbs.reduce(0) { sum, meal in
-                    let mealCarbs = Decimal(string: "\(meal.carbs)") ?? Decimal.zero
-                    return sum + mealCarbs
-                }
-            } catch {
-                debugPrint(
-                    "APSManager: statistics() -> \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) error while fetching carbs"
-                )
-                return 0
-            }
-        }
-    }
-
     private func loopStats(oneDayGlucose: Double) async -> LoopCycles {
         let requestLSR = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
         requestLSR.predicate = NSPredicate(

+ 2 - 2
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -122,12 +122,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @MainActor func deleteGlucoseSource() async {
         cgmManager = nil
         glucoseSource = nil
+        settingsManager.settings.cgm = cgmDefaultModel.type
+        settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
         updateGlucoseSource(
             cgmGlucoseSourceType: cgmDefaultModel.type,
             cgmGlucosePluginId: cgmDefaultModel.id
         )
-        settingsManager.settings.cgm = cgmDefaultModel.type
-        settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
     }
 
     func saveConfigManager() {

+ 17 - 6
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -8,13 +8,15 @@ final class OpenAPS {
     private let processQueue = DispatchQueue(label: "OpenAPS.processQueue", qos: .utility)
 
     private let storage: FileStorage
+    private let tddStorage: TDDStorage
 
     let context = CoreDataStack.shared.newTaskContext()
 
     let jsonConverter = JSONConverter()
 
-    init(storage: FileStorage) {
+    init(storage: FileStorage, tddStorage: TDDStorage) {
         self.storage = storage
+        self.tddStorage = tddStorage
     }
 
     static let dateFormatter: ISO8601DateFormatter = {
@@ -284,7 +286,8 @@ final class OpenAPS {
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
         async let reservoirAsync = loadFileFromStorageAsync(name: Monitor.reservoir)
-        async let preferencesAsync = loadFileFromStorageAsync(name: Settings.preferences)
+        async let preferencesAsync = storage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
+        async let hasSufficientTddForDynamic = tddStorage.hasSufficientTDD()
 
         // Await the results of asynchronous tasks
         let (
@@ -296,7 +299,7 @@ final class OpenAPS {
             basalProfile,
             autosens,
             reservoir,
-            preferences
+            hasSufficientTdd
         ) = await (
             try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
             try carbs,
@@ -306,7 +309,7 @@ final class OpenAPS {
             basalAsync,
             autosenseAsync,
             reservoirAsync,
-            preferencesAsync
+            try hasSufficientTddForDynamic
         )
 
         // Meal calculation
@@ -332,6 +335,14 @@ final class OpenAPS {
             storage.save(iob, as: Monitor.iob)
         }
 
+        var preferences = await preferencesAsync
+
+        if !hasSufficientTdd, preferences.useNewFormula || (preferences.useNewFormula && preferences.sigmoid) {
+            debug(.openAPS, "Insufficient TDD for dynamic formula; disabling for determine basal run.")
+            preferences.useNewFormula = false
+            preferences.sigmoid = false
+        }
+
         // Determine basal
         let orefDetermination = try await determineBasal(
             glucose: glucoseAsJSON,
@@ -348,7 +359,7 @@ final class OpenAPS {
             oref2_variables: oref2_variables
         )
 
-        debug(.openAPS, "Determinated: \(orefDetermination)")
+        debug(.openAPS, "OREF DETERMINATION: \(orefDetermination)")
 
         if var determination = Determination(from: orefDetermination), let deliverAt = determination.deliverAt {
             // set both timestamp and deliverAt to the SAME date; this will be updated for timestamp once it is enacted
@@ -362,7 +373,7 @@ final class OpenAPS {
 
             return determination
         } else {
-            throw APSError.apsError(message: "Determination is nil")
+            throw APSError.apsError(message: "No determination data.")
         }
     }
 

+ 36 - 258
Trio/Sources/APS/PluginManager.swift

@@ -1,279 +1,57 @@
+import CGMBLEKit
 import Foundation
+import G7SensorKit
+import G7SensorKitUI
+import LibreTransmitter
+import LibreTransmitterUI
 import LoopKit
 import LoopKitUI
 import Swinject
 
 protocol PluginManager {
-    var availablePumpManagers: [PumpManagerDescriptor] { get }
     var availableCGMManagers: [CGMManagerDescriptor] { get }
-    var availableServices: [ServiceDescriptor] { get }
-    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type?
     func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type?
-    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type?
 }
 
 class BasePluginManager: Injectable, PluginManager {
-    let pluginBundles: [Bundle]
+    struct CgmPluginDescription {
+        let pluginIdentifier: String
+        let localizedTitle: String
+        let manager: CGMManagerUI.Type
+    }
+
+    static let cgms = [
+        CgmPluginDescription(
+            pluginIdentifier: G5CGMManager.pluginIdentifier,
+            localizedTitle: String(localized: "Dexcom G5"),
+            manager: G5CGMManager.self
+        ),
+        CgmPluginDescription(
+            pluginIdentifier: G6CGMManager.pluginIdentifier,
+            localizedTitle: String(localized: "Dexcom G6 / ONE"),
+            manager: G6CGMManager.self
+        ),
+        CgmPluginDescription(
+            pluginIdentifier: G7CGMManager.pluginIdentifier,
+            localizedTitle: String(localized: "Dexcom G7 / ONE+"),
+            manager: G7CGMManager.self
+        ),
+        CgmPluginDescription(
+            pluginIdentifier: LibreTransmitterManagerV3.pluginIdentifier,
+            localizedTitle: String(localized: "FreeStyle Libre"),
+            manager: LibreTransmitterManagerV3.self
+        )
+    ]
 
     init(resolver: Resolver) {
-        let pluginsURL: URL? = Bundle.main.privateFrameworksURL
-        var bundles = [Bundle]()
-
-        if let pluginsURL = pluginsURL {
-            do {
-                for pluginURL in try FileManager.default.contentsOfDirectory(at: pluginsURL, includingPropertiesForKeys: nil)
-                    .filter({ $0.path.hasSuffix(".framework") })
-                {
-                    if let bundle = Bundle(url: pluginURL) {
-                        if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
-                            debug(.deviceManager, "bundle name: \(bname)")
-                        }
-                        if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
-                            debug(.deviceManager, "bundle is CGM: \(bcgm)")
-                        }
-
-                        if bundle.isLoopPlugin {
-                            debug(.deviceManager, "Found loop plugin:\(pluginURL.absoluteString)")
-                            bundles.append(bundle)
-                        }
-                    }
-                }
-            } catch {
-                debug(.deviceManager, "Error loading plugin: \(error)")
-            }
-        }
-        pluginBundles = bundles
         injectServices(resolver)
     }
 
-    func getPumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? PumpManagerUIPlugin {
-                            return plugin.pumpManagerType
-                        } else {
-                            fatalError("PrincipalClass does not conform to PumpManagerUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-
-    var availablePumpManagers: [PumpManagerDescriptor] {
-        pluginBundles.compactMap({ (bundle) -> PumpManagerDescriptor? in
-            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerDisplayName.rawValue) as? String,
-                  let identifier = bundle
-                  .object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String
-            else {
-                return nil
-            }
-
-            return PumpManagerDescriptor(identifier: identifier, localizedTitle: title)
-        })
-    }
-
-    func getCGMManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? CGMManagerUIPlugin {
-                            return plugin.cgmManagerType
-                        } else {
-                            fatalError("PrincipalClass does not conform to CGMManagerUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
+    func getCGMManagerTypeByIdentifier(_ pluginIdentifier: String) -> CGMManagerUI.Type? {
+        BasePluginManager.cgms.filter({ $0.pluginIdentifier == pluginIdentifier }).first?.manager
     }
 
     var availableCGMManagers: [CGMManagerDescriptor] {
-        pluginBundles.compactMap({ (bundle) -> CGMManagerDescriptor? in
-            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerDisplayName.rawValue) as? String,
-                  let identifier = bundle
-                  .object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String
-            else {
-                return nil
-            }
-
-            return CGMManagerDescriptor(identifier: identifier, localizedTitle: title)
-        })
-    }
-
-    func getServiceTypeByIdentifier(_ identifier: String) -> ServiceUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? ServiceUIPlugin {
-                            return plugin.serviceType
-                        } else {
-                            fatalError("PrincipalClass does not conform to ServiceUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-
-    var availableServices: [ServiceDescriptor] {
-        pluginBundles.compactMap({ (bundle) -> ServiceDescriptor? in
-            guard let title = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceDisplayName.rawValue) as? String,
-                  let identifier = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String
-            else {
-                return nil
-            }
-
-            return ServiceDescriptor(identifier: identifier, localizedTitle: title)
-        })
-    }
-
-    func getStatefulPluginTypeByIdentifier(_ identifier: String) -> StatefulPluggable.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? StatefulPlugin {
-                            return plugin.pluginType
-                        } else {
-                            fatalError("PrincipalClass does not conform to StatefulPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
+        BasePluginManager.cgms.map { CGMManagerDescriptor(identifier: $0.pluginIdentifier, localizedTitle: $0.localizedTitle) }
     }
-
-    var availableStatefulPluginIdentifiers: [String] {
-        pluginBundles.compactMap({ (bundle) -> String? in
-            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String
-        })
-    }
-
-    func getOnboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? OnboardingUIPlugin {
-                            return plugin.onboardingType
-                        } else {
-                            fatalError("PrincipalClass does not conform to OnboardingUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-
-    var availableOnboardingIdentifiers: [String] {
-        pluginBundles.compactMap({ (bundle) -> String? in
-            bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String
-        })
-    }
-
-    func getSupportUITypeByIdentifier(_ identifier: String) -> SupportUI.Type? {
-        for bundle in pluginBundles {
-            if let name = bundle.object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String,
-               name == identifier
-            {
-                do {
-                    try bundle.loadAndReturnError()
-
-                    if let principalClass = bundle.principalClass as? NSObject.Type {
-                        if let plugin = principalClass.init() as? SupportUIPlugin {
-                            return type(of: plugin.support)
-                        } else {
-                            fatalError("PrincipalClass does not conform to SupportUIPlugin")
-                        }
-
-                    } else {
-                        fatalError("PrincipalClass not found")
-                    }
-                } catch {
-                    debug(.deviceManager, "Error loading plugin: \(error)")
-                }
-            }
-        }
-        return nil
-    }
-}
-
-extension Bundle {
-    var isPumpManagerPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.pumpManagerIdentifier.rawValue) as? String != nil }
-
-    var isCGMManagerPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.cgmManagerIdentifier.rawValue) as? String != nil }
-
-    var isStatefulPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.statefulPluginIdentifier.rawValue) as? String != nil }
-
-    var isServicePlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.serviceIdentifier.rawValue) as? String != nil }
-    var isOnboardingPlugin: Bool {
-        object(forInfoDictionaryKey: LoopPluginBundleKey.onboardingIdentifier.rawValue) as? String != nil }
-
-    var isSupportPlugin: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.supportIdentifier.rawValue) as? String != nil }
-
-    var isLoopPlugin: Bool {
-        isPumpManagerPlugin || isCGMManagerPlugin || isStatefulPlugin || isServicePlugin || isOnboardingPlugin || isSupportPlugin
-    }
-
-    var isLoopExtension: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.extensionIdentifier.rawValue) as? String != nil }
-
-    var isSimulator: Bool { object(forInfoDictionaryKey: LoopPluginBundleKey.pluginIsSimulator.rawValue) as? Bool == true }
 }

+ 141 - 113
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 import LoopKitUI
 import Swinject
@@ -10,6 +11,7 @@ protocol TDDStorage {
     ) async throws
         -> TDDResult
     func storeTDD(_ tddResult: TDDResult) async
+    func hasSufficientTDD() async throws -> Bool
 }
 
 /// Structure containing the results of TDD calculations
@@ -26,12 +28,12 @@ struct TDDResult {
 final class BaseTDDStorage: TDDStorage, Injectable {
     @Injected() private var storage: FileStorage!
 
+    private let privateContext = CoreDataStack.shared.newTaskContext()
+
     init(resolver: Resolver) {
         injectServices(resolver)
     }
 
-    private let privateContext = CoreDataStack.shared.newTaskContext()
-
     /// Main function to calculate TDD from pump history and basal profile
     /// - Parameters:
     ///   - pumpManager: Representation of paired pump's PumpManagerUI
@@ -396,117 +398,117 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         return gaps
     }
 
-//    /// Finds gaps between tempBasal events where scheduled basal ran, excluding suspend-resume periods
-//    /// - Parameters:
-//    ///   - tempBasalEvents: Array of pump history events of type tempBasal
-//    ///   - suspendResumePairs: Array of suspend and resume event pairs
-//    /// - Returns: Array of gaps, where each gap has a start and end time
-//    private func findBasalGaps(
-//        in tempBasalEvents: [PumpHistoryEvent],
-//        excluding suspendResumePairs: [(suspend: PumpHistoryEvent, resume: PumpHistoryEvent)]
-//    ) -> [(start: Date, end: Date)] {
-//        guard !tempBasalEvents.isEmpty else {
-//            let startOfDay = Calendar.current.startOfDay(for: Date())
-//            return [(start: startOfDay, end: startOfDay.addingTimeInterval(24 * 60 * 60 - 1))]
-//        }
-//
-//        // Merge temp basal and suspend-resume events into a unified timeline
-//        var timeline = [(start: Date, end: Date, type: EventType)]()
-//
-//        for event in tempBasalEvents {
-//            guard let duration = event.duration else { continue }
-//            let eventEnd = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
-//            timeline.append((start: event.timestamp, end: eventEnd, type: .tempBasal))
-//        }
-//
-//        for suspendResume in suspendResumePairs {
-//            timeline.append((start: suspendResume.suspend.timestamp, end: suspendResume.resume.timestamp, type: .pumpSuspend))
-//        }
-//
-//        // Sort the timeline by start time
-//        timeline.sort { $0.start < $1.start }
-//
-//        // Process the timeline to calculate gaps
-//        var gaps = [(start: Date, end: Date)]()
-//        var lastEndTime = Calendar.current.startOfDay(for: timeline.first!.start)
-//        let endOfDay = lastEndTime.addingTimeInterval(24 * 60 * 60 - 1)
-//
-//        for interval in timeline {
-//            if interval.type == .pumpSuspend {
-//                // Extend lastEndTime for suspend periods
-//                lastEndTime = max(lastEndTime, interval.end)
-//                continue
-//            }
-//
-//            if interval.start > lastEndTime {
-//                // Add a gap if there is a gap between lastEndTime and interval.start
-//                gaps.append((start: lastEndTime, end: interval.start))
-//            }
-//
-//            // Update lastEndTime to the maximum end time encountered
-//            lastEndTime = max(lastEndTime, interval.end)
-//        }
-//
-//        if lastEndTime < endOfDay {
-//            // Add a final gap if the lastEndTime is before the end of the day
-//            gaps.append((start: lastEndTime, end: endOfDay))
-//        }
-//
-//        return gaps
-//    }
-
-//    /// Calculates scheduled basal insulin delivery during gaps between temporary basals
-//    /// - Parameters:
-//    ///   - gaps: Array of time periods where scheduled basal was active
-//    ///   - profile: Basal profile entries defining rates throughout the day
-//    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
-//    /// - Returns: Total insulin delivered via scheduled basal in units
-//    private func calculateScheduledBasalInsulin(
-//        gaps: [(start: Date, end: Date)],
-//        profile: [BasalProfileEntry],
-//        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
-//    ) -> Decimal {
-//        // Initialize cached formatter for time string conversion
-//        let timeFormatter: DateFormatter = {
-//            let formatter = DateFormatter()
-//            formatter.dateFormat = "HH:mm:ss"
-//            return formatter
-//        }()
-//
-//        // Pre-calculate profile switch times for efficient lookup
-//        let profileSwitches = profile.map(\.minutes)
-//
-//        return gaps.reduce(into: Decimal(0)) { totalInsulin, gap in
-//            var currentTime = gap.start
-//
-//            while currentTime < gap.end {
-//                // Find applicable basal rate for the current time
-//                guard let rate = findBasalRate(
-//                    for: timeFormatter.string(from: currentTime),
-//                    in: profile
-//                ) else { break }
-//
-//                // Determine when the rate changes (profile switch or gap end)
-//                let nextSwitchTime = getNextBasalRateSwitch(
-//                    after: currentTime,
-//                    switches: profileSwitches,
-//                    calendar: Calendar.current
-//                ) ?? gap.end
-//                let endTime = min(nextSwitchTime, gap.end)
-//                let durationHours = Decimal(endTime.timeIntervalSince(currentTime)) / 3600
-//
-//                let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
-//                totalInsulin += insulin
-//
-//                debug(
-//                    .apsManager,
-//                    "Scheduled Insulin added: \(insulin) U. Duration: \(durationHours) hrs (\(currentTime)-\(endTime))"
-//                )
-//
-//                currentTime = endTime
-//            }
-//        }
-//    }
+    //    /// Finds gaps between tempBasal events where scheduled basal ran, excluding suspend-resume periods
+    //    /// - Parameters:
+    //    ///   - tempBasalEvents: Array of pump history events of type tempBasal
+    //    ///   - suspendResumePairs: Array of suspend and resume event pairs
+    //    /// - Returns: Array of gaps, where each gap has a start and end time
+    //    private func findBasalGaps(
+    //        in tempBasalEvents: [PumpHistoryEvent],
+    //        excluding suspendResumePairs: [(suspend: PumpHistoryEvent, resume: PumpHistoryEvent)]
+    //    ) -> [(start: Date, end: Date)] {
+    //        guard !tempBasalEvents.isEmpty else {
+    //            let startOfDay = Calendar.current.startOfDay(for: Date())
+    //            return [(start: startOfDay, end: startOfDay.addingTimeInterval(24 * 60 * 60 - 1))]
+    //        }
+    //
+    //        // Merge temp basal and suspend-resume events into a unified timeline
+    //        var timeline = [(start: Date, end: Date, type: EventType)]()
+    //
+    //        for event in tempBasalEvents {
+    //            guard let duration = event.duration else { continue }
+    //            let eventEnd = event.timestamp.addingTimeInterval(TimeInterval(duration * 60))
+    //            timeline.append((start: event.timestamp, end: eventEnd, type: .tempBasal))
+    //        }
+    //
+    //        for suspendResume in suspendResumePairs {
+    //            timeline.append((start: suspendResume.suspend.timestamp, end: suspendResume.resume.timestamp, type: .pumpSuspend))
+    //        }
+    //
+    //        // Sort the timeline by start time
+    //        timeline.sort { $0.start < $1.start }
+    //
+    //        // Process the timeline to calculate gaps
+    //        var gaps = [(start: Date, end: Date)]()
+    //        var lastEndTime = Calendar.current.startOfDay(for: timeline.first!.start)
+    //        let endOfDay = lastEndTime.addingTimeInterval(24 * 60 * 60 - 1)
+    //
+    //        for interval in timeline {
+    //            if interval.type == .pumpSuspend {
+    //                // Extend lastEndTime for suspend periods
+    //                lastEndTime = max(lastEndTime, interval.end)
+    //                continue
+    //            }
+    //
+    //            if interval.start > lastEndTime {
+    //                // Add a gap if there is a gap between lastEndTime and interval.start
+    //                gaps.append((start: lastEndTime, end: interval.start))
+    //            }
+    //
+    //            // Update lastEndTime to the maximum end time encountered
+    //            lastEndTime = max(lastEndTime, interval.end)
+    //        }
+    //
+    //        if lastEndTime < endOfDay {
+    //            // Add a final gap if the lastEndTime is before the end of the day
+    //            gaps.append((start: lastEndTime, end: endOfDay))
+    //        }
+    //
+    //        return gaps
+    //    }
+
+    //    /// Calculates scheduled basal insulin delivery during gaps between temporary basals
+    //    /// - Parameters:
+    //    ///   - gaps: Array of time periods where scheduled basal was active
+    //    ///   - profile: Basal profile entries defining rates throughout the day
+    //    ///   - roundToSupportedBasalRate: Closure to round rates to pump-supported values
+    //    /// - Returns: Total insulin delivered via scheduled basal in units
+    //    private func calculateScheduledBasalInsulin(
+    //        gaps: [(start: Date, end: Date)],
+    //        profile: [BasalProfileEntry],
+    //        roundToSupportedBasalRate: @escaping (_ unitsPerHour: Double) -> Double
+    //    ) -> Decimal {
+    //        // Initialize cached formatter for time string conversion
+    //        let timeFormatter: DateFormatter = {
+    //            let formatter = DateFormatter()
+    //            formatter.dateFormat = "HH:mm:ss"
+    //            return formatter
+    //        }()
+    //
+    //        // Pre-calculate profile switch times for efficient lookup
+    //        let profileSwitches = profile.map(\.minutes)
+    //
+    //        return gaps.reduce(into: Decimal(0)) { totalInsulin, gap in
+    //            var currentTime = gap.start
+    //
+    //            while currentTime < gap.end {
+    //                // Find applicable basal rate for the current time
+    //                guard let rate = findBasalRate(
+    //                    for: timeFormatter.string(from: currentTime),
+    //                    in: profile
+    //                ) else { break }
+    //
+    //                // Determine when the rate changes (profile switch or gap end)
+    //                let nextSwitchTime = getNextBasalRateSwitch(
+    //                    after: currentTime,
+    //                    switches: profileSwitches,
+    //                    calendar: Calendar.current
+    //                ) ?? gap.end
+    //                let endTime = min(nextSwitchTime, gap.end)
+    //                let durationHours = Decimal(endTime.timeIntervalSince(currentTime)) / 3600
+    //
+    //                let insulin = Decimal(roundToSupportedBasalRate(Double(rate * durationHours)))
+    //                totalInsulin += insulin
+    //
+    //                debug(
+    //                    .apsManager,
+    //                    "Scheduled Insulin added: \(insulin) U. Duration: \(durationHours) hrs (\(currentTime)-\(endTime))"
+    //                )
+    //
+    //                currentTime = endTime
+    //            }
+    //        }
+    //    }
 
     /// Finds the next basal rate switch time after a given time
     /// - Parameters:
@@ -634,6 +636,32 @@ final class BaseTDDStorage: TDDStorage, Injectable {
             return weightedTDD.truncated(toPlaces: 3)
         }
     }
+
+    /// Checks if there is enough Total Daily Dose (TDD) data collected over the past 7 days.
+    ///
+    /// This function performs a count fetch for TDDStored records in Core Data where:
+    /// - The record's date is within the last 7 days.
+    /// - The total value is greater than 0.
+    ///
+    /// It then checks if at least 85% of the expected data points are present,
+    /// assuming at least 288 expected entries per day (one every 5 minutes).
+    ///
+    /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
+    /// - Throws: An error if the Core Data count operation fails.
+    func hasSufficientTDD() async throws -> Bool {
+        try await privateContext.perform {
+            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TDDStored")
+            fetchRequest.predicate = NSPredicate(
+                format: "date > %@ AND total > 0",
+                Date().addingTimeInterval(-86400 * 7) as NSDate
+            )
+            fetchRequest.resultType = .countResultType
+
+            let count = try self.privateContext.count(for: fetchRequest)
+            let threshold = Int(Double(7 * 288) * 0.85)
+            return count >= threshold
+        }
+    }
 }
 
 /// Extension for rounding Decimal numbers

+ 1 - 0
Trio/Sources/Application/AppDelegate.swift

@@ -31,6 +31,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
                         .default,
                         "\(DebuggingIdentifiers.failed) failed to handle remote notification with error: \(error.localizedDescription)"
                     )
+                    completionHandler(.failed)
                 }
             }
         } catch {

+ 1 - 2
Trio/Sources/Application/TrioApp.swift

@@ -86,9 +86,8 @@ extension Notification.Name {
 
         debug(
             .default,
-            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [submodules: \(submodulesInfo)]"
+            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [Branch: \(BuildDetails.shared.branchAndSha)] [submodules: \(submodulesInfo)]"
         )
-
         // Fix bug in iOS 18 related to the translucent tab bar
         configureTabBarAppearance()
 

+ 9 - 3
Trio/Sources/Helpers/BuildDetails.swift

@@ -23,10 +23,16 @@ class BuildDetails: Injectable {
         dict["com-trio-build-date"] as? String
     }
 
+    var trioBranch: String {
+        dict["com-trio-branch"] as? String ?? String(localized: "Unknown")
+    }
+
+    var trioCommitSHA: String {
+        dict["com-trio-commit-sha"] as? String ?? String(localized: "Unknown")
+    }
+
     var branchAndSha: String {
-        let branch = dict["com-trio-branch"] as? String ?? String(localized: "Unknown")
-        let sha = dict["com-trio-commit-sha"] as? String ?? String(localized: "Unknown")
-        return "\(branch) \(sha)"
+        "\(trioBranch) \(trioCommitSHA)"
     }
 
     /// Returns a dictionary of submodule details.

File diff ditekan karena terlalu besar
+ 30034 - 6162
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 2 - 2
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -48,7 +48,7 @@ struct DecimalPickerSettings {
     var maxCarbs = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxFat = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
     var maxProtein = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gram)
-    var overrideFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.5, max: 1.5, type: PickerSetting.PickerSettingType.factor)
+    var overrideFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.05, max: 1.5, type: PickerSetting.PickerSettingType.factor)
     var fattyMealFactor = PickerSetting(value: 0.7, step: 0.05, min: 0.05, max: 1, type: PickerSetting.PickerSettingType.factor)
     var sweetMealFactor = PickerSetting(value: 1, step: 0.05, min: 0.05, max: 2, type: PickerSetting.PickerSettingType.factor)
     var maxIOB = PickerSetting(value: 0, step: 1, min: 0, max: 20, type: PickerSetting.PickerSettingType.insulinUnit)
@@ -136,7 +136,7 @@ struct DecimalPickerSettings {
     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 dia = PickerSetting(value: 10, step: 0.5, min: 5, 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: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxBasal = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
 }
 

+ 6 - 0
Trio/Sources/Models/Determination.swift

@@ -19,6 +19,11 @@ struct Determination: JSON, Equatable {
     let reservoir: Decimal?
     var isf: Decimal?
     var timestamp: Date?
+
+    /// `tdd` (Total Daily Dose) is included so it can be part of the
+    /// enacted and suggested devicestatus data that gets uploaded to Nightscout.
+    var tdd: Decimal?
+
     var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
@@ -59,6 +64,7 @@ extension Determination {
         case timestamp
         case isf = "ISF"
         case current_target
+        case tdd = "TDD"
         case insulinForManualBolus
         case manualBolusErrorString
         case minDelta

+ 2 - 1
Trio/Sources/Models/Preferences.swift

@@ -298,8 +298,9 @@ extension Preferences: Decodable {
             preferences.sigmoid = sigmoid
         }
 
+        // FIXME: remove this at a later release; hard code it to false for now
         if let enableDynamicCR = try? container.decode(Bool.self, forKey: .enableDynamicCR) {
-            preferences.enableDynamicCR = enableDynamicCR
+            preferences.enableDynamicCR = false
         }
 
         if let useNewFormula = try? container.decode(Bool.self, forKey: .useNewFormula) {

+ 0 - 16
Trio/Sources/Models/TimeInRangeChartStyle.swift

@@ -1,16 +0,0 @@
-import Foundation
-
-enum TimeInRangeChartStyle: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    var id: String { rawValue }
-    case vertical
-    case horizontal
-
-    var displayName: String {
-        switch self {
-        case .vertical:
-            return String(localized: "Vertical", comment: "")
-        case .horizontal:
-            return String(localized: "Horizontal", comment: "")
-        }
-    }
-}

+ 34 - 0
Trio/Sources/Models/TimeInRangeType.swift

@@ -0,0 +1,34 @@
+import Foundation
+
+enum TimeInRangeType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case timeInTightRange
+    case timeInNormoglycemia
+
+    var displayName: String {
+        switch self {
+        case .timeInTightRange:
+            return String(localized: "Time in Tight Range (TITR)", comment: "")
+
+        case .timeInNormoglycemia:
+            return String(localized: "Time in Normoglycemia (TING)", comment: "")
+        }
+    }
+
+    var bottomThreshold: Int {
+        switch self {
+        case .timeInTightRange:
+            return 70
+        case .timeInNormoglycemia:
+            return 63
+        }
+    }
+
+    var topThreshold: Int {
+        switch self {
+        case .timeInNormoglycemia,
+             .timeInTightRange:
+            return 140
+        }
+    }
+}

+ 10 - 5
Trio/Sources/Models/TrioSettings.swift

@@ -54,7 +54,6 @@ struct TrioSettings: JSON, Equatable {
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
-    var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
     var rulerMarks: Bool = true
     var forecastDisplayType: ForecastDisplayType = .cone
     var maxCarbs: Decimal = 250
@@ -67,9 +66,11 @@ struct TrioSettings: JSON, Equatable {
     var sweetMeals: Bool = false
     var sweetMealFactor: Decimal = 1
     var displayPresets: Bool = true
+    var confirmBolus: Bool = false
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
+    var timeInRangeType: TimeInRangeType = .timeInTightRange
 }
 
 extension TrioSettings: Decodable {
@@ -252,10 +253,6 @@ extension TrioSettings: Decodable {
             settings.yGridLines = yGridLines
         }
 
-        if let timeInRangeChartStyle = try? container.decode(TimeInRangeChartStyle.self, forKey: .timeInRangeChartStyle) {
-            settings.timeInRangeChartStyle = timeInRangeChartStyle
-        }
-
         if let rulerMarks = try? container.decode(Bool.self, forKey: .rulerMarks) {
             settings.rulerMarks = rulerMarks
         }
@@ -288,6 +285,10 @@ extension TrioSettings: Decodable {
             settings.displayPresets = displayPresets
         }
 
+        if let confirmBolus = try? container.decode(Bool.self, forKey: .confirmBolus) {
+            settings.confirmBolus = confirmBolus
+        }
+
         if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
             settings.useLiveActivity = useLiveActivity
         }
@@ -300,6 +301,10 @@ extension TrioSettings: Decodable {
             settings.bolusShortcut = bolusShortcut
         }
 
+        if let timeInRangeType = try? container.decode(TimeInRangeType.self, forKey: .timeInRangeType) {
+            settings.timeInRangeType = timeInRangeType
+        }
+
         self = settings
     }
 }

+ 3 - 0
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -232,6 +232,9 @@ extension Adjustments.StateModel {
                 // execute sequentially instead of concurrently
                 await self.updateLatestOverrideConfigurationOfState(from: id)
                 await self.setCurrentOverride(from: id)
+
+                // perform determine basal sync to immediately apply override changes
+                try await apsManager.determineBasalSync()
             } catch {
                 debug(
                     .default,

+ 3 - 0
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -16,6 +16,9 @@ extension Adjustments.StateModel {
                 async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
                 async let setTempTarget: () = setCurrentTempTarget(from: id)
                 _ = await (updateState, setTempTarget)
+
+                // perform determine basal sync to immediately apply temp target changes
+                try await apsManager.determineBasalSync()
             } catch {
                 debug(
                     .default,

+ 5 - 1
Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift

@@ -96,7 +96,11 @@ extension Adjustments.RootView {
     }
 
     private var deleteConfirmationTitle: String {
-        "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
+        let presetName = selectedTempTarget?.name ?? ""
+        return String(
+            localized: "Delete the Temp Target Preset \"\(presetName)\"?",
+            comment: "Delete confirmation title for temporary target presets"
+        )
     }
 
     private func deleteConfirmationButtons() -> some View {

+ 4 - 0
Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -22,6 +22,8 @@ extension AlgorithmAdvancedSettings {
         @Published var remainingCarbsCap: Decimal = 90
         @Published var noisyCGMTargetMultiplier: Decimal = 1.3
         @Published var insulinActionCurve: Decimal = 10
+        @Published var smbDeliveryRatio: Decimal = 0.5
+        @Published var smbInterval: Decimal = 3
 
         var pumpSettings: PumpSettings {
             provider.settings()
@@ -45,6 +47,8 @@ extension AlgorithmAdvancedSettings {
             subscribePreferencesSetting(\.remainingCarbsCap, on: $remainingCarbsCap) { remainingCarbsCap = $0 }
             subscribePreferencesSetting(\.noisyCGMTargetMultiplier, on: $noisyCGMTargetMultiplier) {
                 noisyCGMTargetMultiplier = $0 }
+            subscribePreferencesSetting(\.smbDeliveryRatio, on: $smbDeliveryRatio) { smbDeliveryRatio = $0 }
+            subscribePreferencesSetting(\.smbInterval, on: $smbInterval) { smbInterval = $0 }
 
             insulinActionCurve = pumpSettings.insulinActionCurve
         }

+ 52 - 0
Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -251,6 +251,58 @@ extension AlgorithmAdvancedSettings {
                 )
 
                 SettingInputSection(
+                    decimalValue: $state.smbDeliveryRatio,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "SMB Delivery Ratio", comment: "SMB Delivery Ratio")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("smbDeliveryRatio"),
+                    label: String(localized: "SMB Delivery Ratio", comment: "SMB Delivery Ratio"),
+                    miniHint: String(localized: "Percentage of calculated insulin required that is given as SMB."),
+                    verboseHint:
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: 50%").bold()
+                        Text(
+                            "Once the total insulin required is calculated, this safety limit specifies what percentage of the insulin required can be delivered as an SMB."
+                        )
+                        Text(
+                            "Due to SMBs potentially occurring every 5 minutes with each loop cycle, it is important to set this value to a reasonable level that allows Trio to safely zero temp should dosing needs suddenly change. Increase this value with caution."
+                        )
+                        Text("Note: Allowed range is 30 - 70%")
+                    }
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.smbInterval,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "SMB Interval", comment: "SMB Interval")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("smbInterval"),
+                    label: String(localized: "SMB Interval", comment: "SMB Interval"),
+                    miniHint: String(localized: "Minimum minutes since the last SMB or manual bolus to allow an automated SMB."),
+                    verboseHint:
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: 3 min").bold()
+                        Text(
+                            "This is the minimum number of minutes since the last SMB or manual bolus before Trio will permit an automated SMB."
+                        )
+                    }
+                )
+
+                SettingInputSection(
                     decimalValue: $state.min5mCarbimpact,
                     booleanValue: $booleanPlaceholder,
                     shouldDisplayHint: $shouldDisplayHint,

+ 5 - 18
Trio/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -9,31 +9,18 @@ extension BolusCalculatorConfig {
         @Published var sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var displayPresets: Bool = true
+        @Published var confirmBolusWhenVeryLowGlucose: Bool = false
 
         override func subscribe() {
             units = settingsManager.settings.units
 
-            subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
-                let value = max(min($0, 1.2), 0.1)
-                overrideFactor = value
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.overrideFactor, on: $overrideFactor) { overrideFactor = $0 }
             subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
             subscribeSetting(\.displayPresets, on: $displayPresets) { displayPresets = $0 }
-            subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
-                let value = max(min($0, 1.2), 0.1)
-                fattyMealFactor = value
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor) { fattyMealFactor = $0 }
             subscribeSetting(\.sweetMeals, on: $sweetMeals) { sweetMeals = $0 }
-            subscribeSetting(\.sweetMealFactor, on: $sweetMealFactor, initial: {
-                let value = max(min($0, 5), 1)
-                sweetMealFactor = value
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.sweetMealFactor, on: $sweetMealFactor) { sweetMealFactor = $0 }
+            subscribeSetting(\.confirmBolus, on: $confirmBolusWhenVeryLowGlucose) { confirmBolusWhenVeryLowGlucose = $0 }
         }
     }
 }

+ 33 - 2
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -113,7 +113,7 @@ extension BolusCalculatorConfig {
                             "When \"Fatty Meal\" is selected in the bolus calculator, the recommended bolus will be multiplied by the \"Fatty Meal Bolus Percentage\" as well as the \"Recommended Bolus Percentage\"."
                         )
                         Text(
-                            "If you have a \"Recommended Bolus Percentage\" of 80%, and a \"Fatty Meal Bolus Percentage\" of 70%, your recommended bolus will be multiplied by: (80 × 70) ÷ 100 = 56%."
+                            "If you have a \"Recommended Bolus Percentage\" of 80%, and a \"Fatty Meal Bolus Percentage\" of 70%, your recommended bolus will be multiplied by: (80 × 70) / 100 = 56%."
                         )
                         Text("This could be useful for slow absorbing meals like pizza.")
                     }
@@ -147,11 +147,42 @@ extension BolusCalculatorConfig {
                             "When \"Super Bolus\" is selected in the bolus calculator, your current basal rate multiplied by \"Super Bolus Percentage\" will be added to your bolus recommendation."
                         )
                         Text(
-                            "If your current basal rate is 0.8 U/hr and \"Super Bolus Percentage\" is set to 200%: 0.8 × (200 ÷ 100) = 1.6 units will be added to your bolus recommendation."
+                            "If your current basal rate is 0.8 U/hr and \"Super Bolus Percentage\" is set to 200%: 0.8 × (200 / 100) = 1.6 units will be added to your bolus recommendation."
                         )
                         Text("This could be useful for fast absorbing meals like sugary cereal.")
                     }
                 )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.confirmBolusWhenVeryLowGlucose,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "Very Low Glucose Bolus Warning")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: String(localized: "Very Low Glucose Warning"),
+                    miniHint: String(
+                        localized: "Warning when bolusing with a very low or forecasted very low glucose."
+                    ),
+                    verboseHint: VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: OFF").bold()
+                        Text(
+                            "Triggers a confirmation dialog if you attempt to bolus when glucose is < \(state.units == .mgdL ? 54.description : 54.formattedAsMmolL) \(state.units.rawValue)."
+                        )
+                        Text(
+                            "Also triggered when the lowest forecasted glucose (minPredBG) is < \(state.units == .mgdL ? 54.description : 54.formattedAsMmolL) \(state.units.rawValue)."
+                        )
+                        Text(
+                            "Note: The forecast used for this warning does not include carbs or insulin that have not yet been logged."
+                        )
+                    }
+                )
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {

+ 60 - 47
Trio/Sources/Modules/CGMSettings/CGMSettingsStateModel.swift

@@ -4,6 +4,9 @@ import G7SensorKit
 import LoopKitUI
 import SwiftUI
 
+/// For a full description of the events that can happen for the CGM lifecycle, see comment at the top
+/// of HomeStateModel+CGM since these are the same events
+
 struct CGMModel: Identifiable, Hashable {
     var id: String
     var type: CGMType
@@ -23,18 +26,6 @@ let cgmDefaultModel = CGMModel(
     subtitle: CGMType.none.subtitle
 )
 
-struct OtherCGMSourceCompletionNotifying: CompletionNotifying {
-    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
-}
-
-class CGMSetupCompletionNotifying: CompletionNotifying {
-    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
-}
-
-class CGMDeletionCompletionNotifying: CompletionNotifying {
-    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
-}
-
 extension CGMSettings {
     final class StateModel: BaseStateModel<Provider> {
         // Singleton implementation
@@ -49,7 +40,7 @@ extension CGMSettings {
 
         @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var pluginCGMManager: PluginManager!
-        @Injected() private var broadcaster: Broadcaster!
+        @Injected() var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
 
         @Published var units: GlucoseUnits = .mgdL
@@ -60,8 +51,11 @@ extension CGMSettings {
         @Published var listOfCGM: [CGMModel] = []
         @Published var url: URL?
 
+        var shouldRunDeleteOnSettingsChange = true
+
         override func subscribe() {
             units = settingsManager.settings.units
+            broadcaster.register(SettingsObserver.self, observer: self)
 
             // collect the list of CGM available with plugins and CGMType defined manually
             listOfCGM = (
@@ -122,28 +116,36 @@ extension CGMSettings {
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
         }
 
+        // this function will get called for all CGM types (plugin and non plugin)
         func addCGM(cgm: CGMModel) {
             cgmCurrent = cgm
-            switch cgmCurrent.type {
+            switch cgm.type {
             case .plugin:
                 shouldDisplayCGMSetupSheet.toggle()
             default:
-                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
+                // non plugin CGM types should be considered onboarded right away
+                shouldDisplayCGMSetupSheet = true
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = ""
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+                broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
             }
         }
 
+        // Note: This function does _not_ get called for plugin CGMs
+        // instead, they will get cgmManagerWantsDeletion events which
+        // are handled by PluginSource
         func deleteCGM() {
-            fetchGlucoseManager.performOnCGMManagerQueue {
-                // Call plugin functionality on the manager queue (or at least attempt to)
-                Task {
-                    await self.fetchGlucoseManager?.deleteGlucoseSource()
-                }
+            Task {
+                await self.fetchGlucoseManager?.deleteGlucoseSource()
 
-                // UI updates go back to Main
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.shouldDisplayCGMSetupSheet = false
-                    self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+                    broadcaster.notify(GlucoseObserver.self, on: .main) {
+                        $0.glucoseDidUpdate([])
+                    }
                 }
             }
         }
@@ -152,40 +154,36 @@ extension CGMSettings {
 
 extension CGMSettings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        // if CGM was deleted
-        if fetchGlucoseManager.cgmGlucoseSourceType == .none {
-            cgmCurrent = cgmDefaultModel
-            settingsManager.settings.cgm = cgmDefaultModel.type
-            settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-            Task {
-                await fetchGlucoseManager.deleteGlucoseSource()
-            }
-            shouldDisplayCGMSetupSheet = false
-        } else {
-            settingsManager.settings.cgm = cgmCurrent.type
-            settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
-            fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
-            shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
-                .type == .xdrip || cgmCurrent.type == .enlite
-        }
-
-        // update glucose source if required
-        DispatchQueue.main.async {
-            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
-                $0.glucoseDidUpdate([])
+        Task {
+            // this sleep is because this event and cgmManagerWantsDeletion
+            // are called in parallel.
+            try await Task.sleep(for: .seconds(0.2))
+            await MainActor.run {
+                if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                    cgmCurrent = cgmDefaultModel
+                }
             }
         }
+        shouldDisplayCGMSetupSheet = false
     }
 }
 
 extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
-        // update the glucose source
+        // cgmCurrent should have been set in addCGM
+        debug(.service, "didCreateCGMManager called \(cgmCurrent)")
+        settingsManager.settings.cgm = cgmCurrent.type
+        settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
         fetchGlucoseManager.updateGlucoseSource(
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucosePluginId: cgmCurrent.id,
             newManager: manager
         )
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
     }
 
     func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
@@ -193,8 +191,23 @@ extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     }
 }
 
-extension CGMSettings.StateModel {
+extension CGMSettings.StateModel: SettingsObserver {
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
+        // Deletes are handled differently for plugins vs non plugins
+        // but both will call deleteGlucoseSource on the fetchGlucoseManager
+        // so we listen for changes to the cgm setting and update our internal
+        // state accordingly
+        if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
+            shouldRunDeleteOnSettingsChange = false
+            cgmCurrent = cgmDefaultModel
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            shouldRunDeleteOnSettingsChange = true
+        }
     }
 }

+ 6 - 3
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -106,8 +106,7 @@ extension CGMSettings {
                         type: .boolean,
                         label: String(localized: "Smooth Glucose Value"),
                         miniHint: String(localized: "Smooth CGM readings using Savitzky-Golay filtering."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
+                        verboseHint: VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
                             Text(
                                 "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
@@ -161,7 +160,11 @@ extension CGMSettings {
                                 completionDelegate: state,
                                 setupDelegate: state,
                                 pluginCGMManager: self.state.pluginCGMManager
-                            )
+                            ).onDisappear {
+                                if state.fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                                    state.cgmCurrent = cgmDefaultModel
+                                }
+                            }
                         }
                     }
                 }

+ 4 - 6
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -140,12 +140,10 @@ extension CGMSettings {
                                 .padding(.vertical)
                         }
 
-                        if state.url == nil {
-                            NavigationLink(
-                                destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
-                                label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
-                            )
-                        }
+                        NavigationLink(
+                            destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                            label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
+                        )
                     }
                 ).listRowBackground(Color.chart)
 

+ 17 - 6
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -367,7 +367,7 @@ extension DataTable {
                                 action: {
                                     alertGlucoseToDelete = glucose
 
-                                    alertTitle = "Delete Glucose?"
+                                    alertTitle = String(localized: "Delete Glucose?", comment: "Alert title for deleting glucose")
                                     alertMessage = Formatter.dateFormatter
                                         .string(from: glucose.date ?? Date()) + ", " +
                                         (Formatter.decimalFormatterWithTwoFractionDigits.string(for: glucose.glucose) ?? "0")
@@ -526,7 +526,7 @@ extension DataTable {
                         role: .none,
                         action: {
                             alertTreatmentToDelete = item
-                            alertTitle = "Delete Insulin?"
+                            alertTitle = String(localized: "Delete Insulin?", comment: "Alert title for deleting insulin")
                             alertMessage = Formatter.dateFormatter
                                 .string(from: item.timestamp ?? Date()) + ", " +
                                 (Formatter.decimalFormatterWithTwoFractionDigits.string(from: item.bolus?.amount ?? 0) ?? "0") +
@@ -534,7 +534,11 @@ extension DataTable {
 
                             if let bolus = item.bolus {
                                 // Add text snippet, so that alert message is more descriptive for SMBs
-                                alertMessage += bolus.isSMB ? " SMB" : ""
+                                alertMessage += bolus.isSMB ? String(
+                                    localized: " SMB",
+                                    comment: "Super Micro Bolus indicator in delete alert"
+                                )
+                                    : ""
                             }
 
                             isRemoveHistoryItemAlertPresented = true
@@ -603,7 +607,7 @@ extension DataTable {
 
                         // meal is carb-only
                         if meal.fpuID == nil {
-                            alertTitle = "Delete Carbs?"
+                            alertTitle = String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
                             alertMessage = Formatter.dateFormatter
                                 .string(from: meal.date ?? Date()) + ", " +
                                 (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
@@ -611,8 +615,15 @@ extension DataTable {
                         }
                         // meal is complex-meal or fpu-only
                         else {
-                            alertTitle = meal.isFPU ? "Delete Carbs Equivalents?" : "Delete Carbs?"
-                            alertMessage = "All FPUs and the carbs of the meal will be deleted."
+                            alertTitle = meal.isFPU ? String(
+                                localized: "Delete Carbs Equivalents?",
+                                comment: "Alert title for deleting carb equivalents"
+                            )
+                                : String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
+                            alertMessage = String(
+                                localized: "All FPUs and the carbs of the meal will be deleted.",
+                                comment: "Alert message for meal deletion"
+                            )
                         }
 
                         isRemoveHistoryItemAlertPresented = true

+ 118 - 6
Trio/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift

@@ -1,3 +1,5 @@
+import Combine
+import CoreData
 import Observation
 import SwiftUI
 
@@ -5,29 +7,117 @@ extension DynamicSettings {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settings: SettingsManager!
         @Injected() var storage: FileStorage!
+        @Injected() var tddStorage: TDDStorage!
 
+        // this is an *interim* fix to provide better UI/UX
+        // FIXME: needs to be refactored, once oref-swift lands and dynamicISF becomes swift-bound
+        @Published var dynamicSensitivityType: DynamicSensitivityType = .disabled {
+            didSet {
+                switch dynamicSensitivityType {
+                case .logarithmic:
+                    useNewFormula = true
+                    sigmoid = false
+                case .sigmoid:
+                    useNewFormula = true
+                    sigmoid = true
+                default:
+                    useNewFormula = false
+                    sigmoid = false
+                }
+            }
+        }
+
+        @Published var hasValidTDD: Bool = false
         @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
+
+        @ObservedObject var pickerSettingsProvider = PickerSettingsProvider.shared
 
         var units: GlucoseUnits = .mgdL
 
+        let context = CoreDataStack.shared.newTaskContext()
+
         override func subscribe() {
             units = settingsManager.settings.units
 
-            subscribePreferencesSetting(\.useNewFormula, on: $useNewFormula) { useNewFormula = $0 }
-            subscribePreferencesSetting(\.enableDynamicCR, on: $enableDynamicCR) { enableDynamicCR = $0 }
-            subscribePreferencesSetting(\.sigmoid, on: $sigmoid) { sigmoid = $0 }
+            /// DynamicISF handling
+            /// Initially, load once from storage and infer `dynamicSensitivityType` based on values of `useNewFormula` (log) and/or `sigmoid`
+            let storedUseNewFormula = settingsManager.preferences.useNewFormula
+            let storedSigmoid = settingsManager.preferences.sigmoid
+            inferDynamicSensitivityType(useNewFormula: storedUseNewFormula, sigmoid: storedSigmoid)
+            /// Subsequently, subscribe to changes from the UI and persist them in the (kept for now) two variables
+            subscribePreferencesSetting(\.useNewFormula, on: $useNewFormula) { _ in }
+            subscribePreferencesSetting(\.sigmoid, on: $sigmoid) { _ in }
+
             subscribePreferencesSetting(\.adjustmentFactor, on: $adjustmentFactor) { adjustmentFactor = $0 }
             subscribePreferencesSetting(\.adjustmentFactorSigmoid, on: $adjustmentFactorSigmoid) { adjustmentFactorSigmoid = $0 }
             subscribePreferencesSetting(\.weightPercentage, on: $weightPercentage) { weightPercentage = $0 }
             subscribePreferencesSetting(\.tddAdjBasal, on: $tddAdjBasal) { tddAdjBasal = $0 }
-            subscribePreferencesSetting(\.threshold_setting, on: $threshold_setting) { threshold_setting = $0 }
+
+            Task {
+                do {
+                    let hasValidTDD = try await tddStorage.hasSufficientTDD()
+                    await MainActor.run {
+                        self.hasValidTDD = hasValidTDD
+                    }
+                } catch {
+                    debug(.coreData, "Error when fetching TDD for validity checking: \(error)")
+                    await MainActor.run {
+                        hasValidTDD = false
+                    }
+                }
+            }
+        }
+
+        /// Infers the `dynamicSensitivityType` based on the stored values of `useNewFormula` and `sigmoid`.
+        /// - Logic:
+        ///   - If `useNewFormula` is `true` and `sigmoid` is `false`, sets type to `.logarithmic`.
+        ///   - If both `useNewFormula` and `sigmoid` are `true`, sets type to `.sigmoid`.
+        ///   - Otherwise, sets type to `.disabled`.
+        ///
+        /// This is used at startup to derive the dynamic sensitivity state from persisted values until
+        /// a future refactor makes `dynamicSensitivityType` a first-class stored preference.
+        // FIXME: needs to be refactored, once oref-swift lands and dynamicISF becomes swift-bound
+        private func inferDynamicSensitivityType(useNewFormula: Bool, sigmoid: Bool) {
+            if useNewFormula {
+                dynamicSensitivityType = sigmoid ? .sigmoid : .logarithmic
+            } else {
+                dynamicSensitivityType = .disabled
+            }
+        }
+
+        /// Checks if there is enough Total Daily Dose (TDD) data collected over the past 7 days.
+        ///
+        /// This function performs a count fetch for TDDStored records in Core Data where:
+        /// - The record's date is within the last 7 days.
+        /// - The total value is greater than 0.
+        ///
+        /// It then checks if at least 85% of the expected data points are present,
+        /// assuming at least 288 expected entries per day (one every 5 minutes).
+        ///
+        /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
+        /// - Throws: An error if the Core Data count operation fails.
+        private func hasSufficientTDD() throws -> Bool {
+            var result = false
+
+            context.performAndWait {
+                let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TDDStored")
+                fetchRequest.predicate = NSPredicate(
+                    format: "date > %@ AND total > 0",
+                    Date().addingTimeInterval(-86400 * 7) as NSDate
+                )
+                fetchRequest.resultType = .countResultType
+
+                let count = (try? context.count(for: fetchRequest)) ?? 0
+                let threshold = Int(Double(7 * 288) * 0.85)
+                result = count >= threshold
+            }
+
+            return result
         }
     }
 }
@@ -37,3 +127,25 @@ extension DynamicSettings.StateModel: SettingsObserver {
         units = settingsManager.settings.units
     }
 }
+
+extension DynamicSettings {
+    enum DynamicSensitivityType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+        var id: String { rawValue }
+        case disabled
+        case logarithmic
+        case sigmoid
+
+        var displayName: String {
+            switch self {
+            case .disabled:
+                return String(localized: "Disabled")
+
+            case .logarithmic:
+                return String(localized: "Logarithmic")
+
+            case .sigmoid:
+                return String(localized: "Sigmoid")
+            }
+        }
+    }
+}

+ 126 - 179
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -41,111 +41,104 @@ extension DynamicSettings {
 
         var body: some View {
             List {
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.useNewFormula,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Activate Dynamic Sensitivity (Dynamic ISF)")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Activate Dynamic ISF"),
-                    miniHint: String(
-                        localized: "Dynamically adjust insulin sensitivity using Dynamic Ratio rather than Autosens Ratio."
-                    ),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text(
-                            "Enabling this feature allows Trio to calculate a new Insulin Sensitivity Factor with each loop cycle by considering your current glucose, the weighted total daily dose of insulin, the set adjustment factor, and a few other data points. This helps tailor your insulin response more accurately in real time."
-                        )
-                        Text(
-                            "Dynamic ISF produces a Dynamic Ratio, replacing the Autosens Ratio, determining how much your profile ISF will be adjusted every loop cycle, ensuring it stays within safe limits set by your Autosens Min/Max settings. It provides more precise insulin dosing by responding to changes in insulin needs throughout the day."
-                        )
-                        Text(
-                            "You can influence the adjustments made by Dynamic ISF primarily by adjusting Autosens Max, Autosens Min, and Adjustment Factor. Other settings also influence Dynamic ISF's response, such as Glucose Target, Profile ISF, Peak Insulin Time, and Weighted Average of TDD."
-                        )
-                        Text(
-                            "Warning: Before adjusting these settings, make sure you are fully aware of the impact those changes will have."
-                        )
-                        .bold()
-                    },
-                    headerText: String(localized: "Dynamic Settings")
-                )
-
-                if state.useNewFormula {
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.enableDynamicCR,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Activate Dynamic CR (Carb Ratio)")
+                Section(
+                    header: Text("Dynamic Insulin Sensitivity"),
+                    content: {
+                        VStack(alignment: .leading) {
+                            Picker(
+                                selection: $state.dynamicSensitivityType,
+                                label: Text("Dynamic ISF").multilineTextAlignment(.leading)
+                            ) {
+                                ForEach(DynamicSensitivityType.allCases) { selection in
+                                    Text(selection.displayName).tag(selection)
+                                }
                             }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: String(localized: "Activate Dynamic CR (Carb Ratio)"),
-                        miniHint: String(localized: "Dynamically adjust your Carb Ratio (CR)."),
-                        verboseHint:
+                            .disabled(!state.hasValidTDD)
+                            .padding(.top)
 
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text(
-                                "Dynamic CR adjusts your carb ratio based on your Dynamic Ratio, adapting automatically to changes in insulin sensitivity."
-                            )
-                            Text(
-                                "When Dynamic Ratio increases, indicating you need more insulin, the carb ratio value is decreased to make your insulin dosing more effective."
-                            )
-                            Text(
-                                "When Dynamic Ratio decreases, indicating you need less insulin, the carb ratio value is increased to avoid over-delivery."
-                            )
-                        }
-                    )
+                            HStack(alignment: .center) {
+                                let miniHintText = state.hasValidTDD ?
+                                    String(
+                                        localized: "Dynamically adjust insulin sensitivity using Dynamic Ratio rather than Autosens Ratio."
+                                    ) :
+                                    String(
+                                        localized: "Trio has only been actively used and looping for less than seven days. Cannot enable dynamic ISF."
+                                    )
+                                let miniHintTextColorForDisabled: Color = colorScheme == .dark ? .orange :
+                                    .accentColor
+                                let miniHintTextColor: Color = state.hasValidTDD ? .secondary : miniHintTextColorForDisabled
 
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.sigmoid,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Use Sigmoid Formula")
-                            }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: String(localized: "Use Sigmoid Formula"),
-                        miniHint: String(localized: "Adjust insulin sensitivity using a sigmoid-shaped curve."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text(
-                                "Turning on the Sigmoid Formula setting alters how your Dynamic Ratio, and thus your New ISF and New Carb Ratio, are calculated using a sigmoid curve rather than the default logarithmic function."
-                            )
-                            Text(
-                                "The curve's steepness is influenced by the Adjustment Factor, while the Autosens Min/Max settings determine the limits of the ratio adjustment, which can also influence the steepness of the sigmoid curve."
-                            )
-                            Text(
-                                "When using the Sigmoid Formula, the weighted Total Daily Dose has a much lower impact on the dynamic adjustments to sensitivity."
-                            )
-                            Text("Careful tuning is essential to avoid overly aggressive insulin changes.")
-                            Text("It is not recommended to set Autosens Max above 150% to maintain safe insulin dosing.")
-                            Text(
-                                "There has been no empirical data analysis to support the use of the Sigmoid Formula for dynamic sensitivity determination."
-                            ).bold()
-                        }
-                    )
+                                Text(miniHintText)
+                                    .font(.footnote)
+                                    .foregroundColor(miniHintTextColor)
+                                    .lineLimit(nil)
+
+                                Spacer()
+                                Button(
+                                    action: {
+                                        hintLabel = String(localized: "Time in Range Chart Style")
+                                        selectedVerboseHint =
+                                            AnyView(
+                                                VStack(alignment: .leading, spacing: 10) {
+                                                    Text("Default: Disabled").bold()
+                                                    Text(
+                                                        "Enabling this feature allows Trio to calculate a new Insulin Sensitivity Factor with each loop cycle dynamically. Trio offers two dynamic formulas:"
+                                                    )
+                                                    VStack(alignment: .leading, spacing: 10) {
+                                                        Text("Logarithmic Dynamic ISF").bold()
+                                                        Text(
+                                                            "Enabling this feature allows Trio to calculate a new Insulin Sensitivity Factor with each loop cycle by considering your current glucose, the weighted total daily dose of insulin, the set adjustment factor, and a few other data points. This helps tailor your insulin response more accurately in real time."
+                                                        )
+                                                        Text(
+                                                            "Dynamic ISF produces a Dynamic Ratio, replacing the Autosens Ratio, determining how much your profile ISF will be adjusted every loop cycle, ensuring it stays within safe limits set by your Autosens Min/Max settings. It provides more precise insulin dosing by responding to changes in insulin needs throughout the day."
+                                                        )
+                                                        Text(
+                                                            "You can influence the adjustments made by Dynamic ISF primarily by adjusting Autosens Max, Autosens Min, and Adjustment Factor. Other settings also influence Dynamic ISF's response, such as Glucose Target, Profile ISF, Peak Insulin Time, and Weighted Average of TDD."
+                                                        )
+                                                        Text(
+                                                            "Warning: Before adjusting these settings, make sure you are fully aware of the impact those changes will have."
+                                                        )
+                                                        .bold()
+                                                    }
 
-                    if !state.sigmoid {
+                                                    VStack(alignment: .leading, spacing: 10) {
+                                                        Text("Sigmoid Dynamic ISF").bold()
+                                                        Text(
+                                                            "Turning on the Sigmoid Formula setting alters how your Dynamic Ratio, and thus your New ISF, are calculated using a sigmoid curve."
+                                                        )
+                                                        Text(
+                                                            "The curve's steepness is influenced by the Adjustment Factor, while the Autosens Min/Max settings determine the limits of the ratio adjustment, which can also influence the steepness of the sigmoid curve."
+                                                        )
+                                                        Text(
+                                                            "When using the Sigmoid Formula, the weighted Total Daily Dose has a much lower impact on the dynamic adjustments to sensitivity."
+                                                        )
+                                                        Text(
+                                                            "Careful tuning is essential to avoid overly aggressive insulin changes."
+                                                        )
+                                                        Text(
+                                                            "It is not recommended to set Autosens Max above 150% to maintain safe insulin dosing."
+                                                        )
+                                                        Text(
+                                                            "There has been no empirical data analysis to support the use of the Sigmoid Formula for dynamic sensitivity determination."
+                                                        ).bold()
+                                                    }
+                                                }
+                                            )
+                                        shouldDisplayHint.toggle()
+                                    },
+                                    label: {
+                                        HStack {
+                                            Image(systemName: "questionmark.circle")
+                                        }
+                                    }
+                                ).buttonStyle(BorderlessButtonStyle())
+                            }.padding(.top)
+                        }.padding(.bottom)
+                    }
+                ).listRowBackground(Color.chart)
+
+                if state.dynamicSensitivityType != .disabled {
+                    if state.dynamicSensitivityType == .logarithmic {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactor,
                             booleanValue: $booleanPlaceholder,
@@ -176,6 +169,35 @@ extension DynamicSettings {
                                 )
                             }
                         )
+
+                        SettingInputSection(
+                            decimalValue: $state.weightPercentage,
+                            booleanValue: $booleanPlaceholder,
+                            shouldDisplayHint: $shouldDisplayHint,
+                            selectedVerboseHint: Binding(
+                                get: { selectedVerboseHint },
+                                set: {
+                                    selectedVerboseHint = $0.map { AnyView($0) }
+                                    hintLabel = String(localized: "Weighted Average of TDD")
+                                }
+                            ),
+                            units: state.units,
+                            type: .decimal("weightPercentage"),
+                            label: String(localized: "Weighted Average of TDD"),
+                            miniHint: String(localized: "Weight of 24-hr TDD against 10-day TDD."),
+                            verboseHint:
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text("Default: 35%").bold()
+                                Text(
+                                    "This setting adjusts how much weight is given to your recent total daily insulin dose when calculating Dynamic ISF and Dynamic CR."
+                                )
+                                Text(
+                                    "At the default setting, 35% of the calculation is based on the last 24 hours of insulin use, with the remaining 65% considering the last 10 days of data."
+                                )
+                                Text("Setting this to 100% means only the past 24 hours will be used.")
+                                Text("A lower value smooths out these variations for more stability.")
+                            }
+                        )
                     } else {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactorSigmoid,
@@ -212,35 +234,6 @@ extension DynamicSettings {
                     }
 
                     SettingInputSection(
-                        decimalValue: $state.weightPercentage,
-                        booleanValue: $booleanPlaceholder,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Weighted Average of TDD")
-                            }
-                        ),
-                        units: state.units,
-                        type: .decimal("weightPercentage"),
-                        label: String(localized: "Weighted Average of TDD"),
-                        miniHint: String(localized: "Weight of 24-hr TDD against 10-day TDD."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: 35%").bold()
-                            Text(
-                                "This setting adjusts how much weight is given to your recent total daily insulin dose when calculating Dynamic ISF and Dynamic CR."
-                            )
-                            Text(
-                                "At the default setting, 35% of the calculation is based on the last 24 hours of insulin use, with the remaining 65% considering the last 10 days of data."
-                            )
-                            Text("Setting this to 100% means only the past 24 hours will be used.")
-                            Text("A lower value smooths out these variations for more stability.")
-                        }
-                    )
-
-                    SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.tddAdjBasal,
                         shouldDisplayHint: $shouldDisplayHint,
@@ -263,56 +256,10 @@ extension DynamicSettings {
                             Text(
                                 "Enabling Adjust Basal replaces the standard Autosens Ratio calculation with its own Autosens Ratio calculated as such:"
                             )
-                            Text("Autosens Ratio =\n(Weighted Average of TDD) ÷ (10-day Average of TDD)")
+                            Text("Autosens Ratio =\n(Weighted Average of TDD) / (10-day Average of TDD)")
                             Text("New Basal Profile =\n(Current Basal Profile) × (Autosens Ratio)")
-                        }
-                    )
-
-                    SettingInputSection(
-                        decimalValue: $state.threshold_setting,
-                        booleanValue: $booleanPlaceholder,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Minimum Safety Threshold")
-                            }
-                        ),
-                        units: state.units,
-                        type: .decimal("threshold_setting"),
-                        label: String(localized: "Minimum Safety Threshold"),
-                        miniHint: String(localized: "Increase the safety threshold used to suspend insulin delivery."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: Set by Algorithm").bold()
-                            Text(
-                                "Minimum Threshold Setting is, by default, determined by your set Glucose Target. This threshold automatically suspends insulin delivery if your glucose levels are forecasted to fall below this value. It’s designed to protect against hypoglycemia, particularly during sleep or other vulnerable times."
-                            )
-                            Text(
-                                "Trio will use the larger of the default setting calculation below and the value entered here."
-                            )
-                            VStack(alignment: .leading, spacing: 10) {
-                                VStack(alignment: .leading, spacing: 5) {
-                                    Text("The default setting is based on this calculation:").bold()
-                                    Text("TargetGlucose - 0.5 × (TargetGlucose - 40)")
-                                }
-                                VStack(alignment: .leading, spacing: 5) {
-                                    Text(
-                                        "If your glucose target is \(state.units == .mgdL ? "110" : 110.formattedAsMmolL) \(state.units.rawValue), Trio will use a safety threshold of \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue), unless you set Minimum Safety Threshold to something > \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue)."
-                                    )
-                                    Text(
-                                        "\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - 0.5 × (\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - \(state.units == .mgdL ? "40" : 40.formattedAsMmolL)) = \(state.units == .mgdL ? "75" : 75.formattedAsMmolL)"
-                                    )
-                                }
-                                Text(
-                                    "This setting is limited to values between \(state.units == .mgdL ? "60" : 60.formattedAsMmolL) - \(state.units == .mgdL ? "120" : 120.formattedAsMmolL) \(state.units.rawValue)"
-                                )
-                                Text(
-                                    "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
-                                )
-                            }
-                        }
+                        },
+                        headerText: String(localized: "Dynamic-dependent Features")
                     )
                 }
             }

+ 2 - 0
Trio/Sources/Modules/GeneralSettings/UnitsLimitsSettingsStateModel.swift

@@ -14,6 +14,7 @@ extension UnitsLimitsSettings {
         @Published var maxIOB: Decimal = 0
         @Published var maxCOB: Decimal = 120
         @Published var hasChanged: Bool = false
+        @Published var threshold_setting: Decimal = 60
 
         var preferences: Preferences {
             settingsManager.preferences
@@ -31,6 +32,7 @@ extension UnitsLimitsSettings {
 
             subscribePreferencesSetting(\.maxIOB, on: $maxIOB) { maxIOB = $0 }
             subscribePreferencesSetting(\.maxCOB, on: $maxCOB) { maxCOB = $0 }
+            subscribePreferencesSetting(\.threshold_setting, on: $threshold_setting) { threshold_setting = $0 }
 
             maxBasal = pumpSettings.maxBasal
             maxBolus = pumpSettings.maxBolus

+ 48 - 1
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -80,7 +80,7 @@ extension UnitsLimitsSettings {
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 10 units").bold()
                         Text(
-                            "This is the maximum bolus allowed to be delivered at one time. This limits manual and automatic bolus."
+                            "This is the maximum bolus allowed to be delivered at one time. This only limits manual boluses and does not limit SMBs."
                         )
                         Text("Most set this to their largest meal bolus. Then, adjust if needed.")
                         Text("If you attempt to request a bolus larger than this, the bolus will not be accepted.")
@@ -141,6 +141,53 @@ extension UnitsLimitsSettings {
                         Text("This is an important limit when UAM is ON.")
                     }
                 )
+
+                SettingInputSection(
+                    decimalValue: $state.threshold_setting,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "Minimum Safety Threshold")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("threshold_setting"),
+                    label: String(localized: "Minimum Safety Threshold"),
+                    miniHint: String(localized: "Increase the safety threshold used to suspend insulin delivery."),
+                    verboseHint:
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: Set by Algorithm").bold()
+                        Text(
+                            "Minimum Threshold Setting is, by default, determined by your set Glucose Target. This threshold automatically suspends insulin delivery if your glucose levels are forecasted to fall below this value. It’s designed to protect against hypoglycemia, particularly during sleep or other vulnerable times."
+                        )
+                        Text(
+                            "Trio will use the larger of the default setting calculation below and the value entered here."
+                        )
+                        VStack(alignment: .leading, spacing: 10) {
+                            VStack(alignment: .leading, spacing: 5) {
+                                Text("The default setting is based on this calculation:").bold()
+                                Text("TargetGlucose - 0.5 × (TargetGlucose - 40)")
+                            }
+                            VStack(alignment: .leading, spacing: 5) {
+                                Text(
+                                    "If your glucose target is \(state.units == .mgdL ? "110" : 110.formattedAsMmolL) \(state.units.rawValue), Trio will use a safety threshold of \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue), unless you set Minimum Safety Threshold to something > \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue)."
+                                )
+                                Text(
+                                    "\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - 0.5 × (\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - \(state.units == .mgdL ? "40" : 40.formattedAsMmolL)) = \(state.units == .mgdL ? "75" : 75.formattedAsMmolL)"
+                                )
+                            }
+                            Text(
+                                "This setting is limited to values between \(state.units == .mgdL ? "60" : 60.formattedAsMmolL) - \(state.units == .mgdL ? "120" : 120.formattedAsMmolL) \(state.units.rawValue)"
+                            )
+                            Text(
+                                "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
+                            )
+                        }
+                    }
+                )
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {

+ 79 - 0
Trio/Sources/Modules/Home/HomeStateModel+CGM.swift

@@ -0,0 +1,79 @@
+import LoopKitUI
+
+/// Notes on the CGM lifecycle:
+/// There are two classes of CGM devices: plugins and non-plugins. Plugins are implemented using
+/// LoopKit APIs and include most hardware CGMs like Dexcom G6, G7, Libre, and so on. Non-plugins
+/// drivers are implemented directly in Trio, and include the CGM Simulator and Nightscout CGM. For
+/// these different CGMs, there are a few different events, handled in different places, that happen to
+/// signify a change in the CGM lifecycle.
+///
+/// Both:
+/// - addCGM function invocation: Called by the UI in response to a user clicking the "add CGM" button
+///
+/// Non-plugins only:
+/// - deleteCGM function invocation: Called by the CGM View in response to a user clicking the "delete CGM" button
+///
+/// Plugins only:
+/// - completionNotifyingDidComplete: Called by the CGM driver to signify that Trio should close its UIViewController
+/// - cgmManagerOnboarding didCreateCGMManager: Called by the CGM driver after adding a new CGM
+/// - cgmManagerWantsDeletion: Called by the CGM driver when the user asks to delete a CGM
+/// There are no ordering constraints between completionNotifyingDidComplete and the other two
+/// Plugin events (it's up to the implementation of each individual driver). For example, the G7 driver invokes
+/// cgmManagerWantsDeletion on the delegate's queue while calling completionNotifyingDidComplete in parallel
+/// on the main queue.
+///
+/// In additinon to having different events for different types of CGMs, the handling of these events is spread out
+/// across various state managers, like HomeStateModel, CGMSettingsStateModel, and PluginSource.
+///
+/// There is CGM state in the HomeStateModel and CGMSettingsStateModel, FetchGlucoseManager, and
+/// SettingsManger
+///
+/// The flow for adding a CGM:
+/// - Non-plugin: addCGM (considered onboarded at this point)
+/// - Plugin: addCGM -> cgmManagerOnboarding (after success)
+///
+/// For deleting a CGM:
+/// - Non-plugin: deleteCGM (in HomeStateModel and CGMSettingsStateModel)
+/// - Plugin: cgmManagerWantsDeletion (in PluginSource)
+/// Then, both non-plugin and plugin:  set settings.cgm (in FetchGlucoseManager) ->
+///     settingsDidChange (in HomeStateModel and CGMSettingsStateModel)
+
+extension Home.StateModel: CompletionDelegate {
+    /// This completion handler is called by both the CGM and the pump
+    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
+        debug(.service, "Completion fired by: \(type(of: notifying))")
+        Task {
+            // this sleep is because this event and cgmManagerWantsDeletion
+            // are called in parallel.
+            try await Task.sleep(for: .seconds(0.2))
+            await MainActor.run {
+                if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                    cgmCurrent = cgmDefaultModel
+                }
+            }
+        }
+        shouldDisplayCGMSetupSheet = false
+        shouldDisplayPumpSetupSheet = false
+    }
+}
+
+extension Home.StateModel: CGMManagerOnboardingDelegate {
+    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
+        settingsManager.settings.cgm = cgmCurrent.type
+        settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
+    }
+
+    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
+        // nothing to do
+    }
+}

+ 1 - 2
Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift

@@ -93,9 +93,8 @@ extension Home.StateModel {
 
             // 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)
+                    await self.updateIobChartBounds(minValue: minIob, maxValue: maxIob)
                 }
             } else {
                 Task {

+ 29 - 64
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -63,6 +63,7 @@ extension Home {
         var alarm: GlucoseAlarm?
         var manualTempBasal = false
         var isSmoothingEnabled = false
+        var maxIOB: Decimal = 0.0
         var autosensMax: Decimal = 1.2
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
@@ -103,6 +104,7 @@ extension Home {
         var cgmAvailable: Bool = false
         var listOfCGM: [CGMModel] = []
         var cgmCurrent = cgmDefaultModel
+        var shouldRunDeleteOnSettingsChange = true
 
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
@@ -394,6 +396,7 @@ extension Home {
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+            maxIOB = settingsManager.preferences.maxIOB
         }
 
         @MainActor private func setupCGMSettings() async {
@@ -455,8 +458,13 @@ extension Home {
             case .plugin:
                 shouldDisplayCGMSetupSheet = true
             default:
-                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(CGMSetupCompletionNotifying())
+                shouldDisplayCGMSetupSheet = true
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = ""
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+                broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
             }
         }
 
@@ -465,12 +473,14 @@ extension Home {
                 // Call plugin functionality on the manager queue (or at least attempt to)
                 Task {
                     await self.fetchGlucoseManager?.deleteGlucoseSource()
-                }
 
-                // UI updates go back to Main
-                DispatchQueue.main.async {
-                    self.shouldDisplayCGMSetupSheet = false
-                    self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+                    // UI updates go back to Main
+                    await MainActor.run {
+                        self.shouldDisplayCGMSetupSheet = false
+                        self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                            $0.glucoseDidUpdate([])
+                        }
+                    }
                 }
             }
         }
@@ -643,6 +653,17 @@ extension Home.StateModel:
         Task {
             await setupCGMSettings()
         }
+        if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
+            shouldRunDeleteOnSettingsChange = false
+            cgmCurrent = cgmDefaultModel
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            shouldRunDeleteOnSettingsChange = true
+        }
     }
 
     func preferencesDidChange(_: Preferences) {
@@ -651,6 +672,7 @@ extension Home.StateModel:
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         isExerciseModeActive = settingsManager.preferences.exerciseMode
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
+        maxIOB = settingsManager.preferences.maxIOB
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {
@@ -685,48 +707,6 @@ extension Home.StateModel:
     }
 }
 
-extension Home.StateModel: CompletionDelegate {
-    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
-        debug(.service, "Completion fired by: \(type(of: notifying))")
-        shouldDisplayCGMSetupSheet = false
-
-        if notifying is CGMSetupCompletionNotifying || notifying is CGMDeletionCompletionNotifying ||
-            notifying is CGMManagerSettingsNavigationViewController || notifying is any SetupTableViewControllerDelegate ||
-            notifying is any CGMManagerOnboarding
-        {
-            if fetchGlucoseManager.cgmGlucoseSourceType == .none {
-                debug(.service, "CGMDeletionCompletionNotifying: CGM Deletion Completed")
-
-                cgmCurrent = cgmDefaultModel
-                settingsManager.settings.cgm = cgmDefaultModel.type
-                settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-                Task {
-                    await fetchGlucoseManager.deleteGlucoseSource()
-                }
-            } else {
-                debug(.service, "CGMSetupCompletionNotifying: CGM Setup Completed")
-
-                settingsManager.settings.cgm = cgmCurrent.type
-                settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
-                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
-
-                shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
-                    .type == .xdrip || cgmCurrent.type == .enlite
-            }
-
-            // update glucose source if required
-            DispatchQueue.main.async {
-                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
-                    $0.glucoseDidUpdate([])
-                }
-            }
-        } else {
-            // pump related handling
-            shouldDisplayPumpSetupSheet = false // hides sheet
-        }
-    }
-}
-
 extension Home.StateModel: PumpManagerOnboardingDelegate {
     func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
         provider.apsManager.pumpManager = pumpManager
@@ -743,18 +723,3 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // nothing to do
     }
 }
-
-extension Home.StateModel: CGMManagerOnboardingDelegate {
-    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
-        // update the glucose source
-        fetchGlucoseManager.updateGlucoseSource(
-            cgmGlucoseSourceType: cgmCurrent.type,
-            cgmGlucosePluginId: cgmCurrent.id,
-            newManager: manager
-        )
-    }
-
-    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
-        // nothing to do
-    }
-}

+ 16 - 5
Trio/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift

@@ -54,8 +54,10 @@ extension MainChartView {
     }
 
     func combinedYDomain() -> ClosedRange<Double> {
-        let minValue = min(state.minValueCobChart, state.minValueIobChart)
-        let maxValue = max(state.maxValueCobChart, state.maxValueIobChart)
+        let iobMin = scaleIobAmountForChart(state.minValueIobChart)
+        let iobMax = scaleIobAmountForChart(state.maxValueIobChart)
+        let minValue = min(state.minValueCobChart, iobMin)
+        let maxValue = max(state.maxValueCobChart, iobMax)
         return Double(minValue) ... Double(maxValue)
     }
 
@@ -79,6 +81,17 @@ extension MainChartView {
         .position(by: .value("Axis", axis))
     }
 
+    /// Scales IOB amounts for chart display.
+    ///
+    /// As IOB and COB share the same Y axis and COB is usually >> IOB,
+    /// we need to visually weigh IOB by multiplying it by a scaling factor:
+    ///
+    /// - Parameter rawAmount: The unscaled IOB amount
+    /// - Returns: The scaled IOB amount for visual representation
+    private func scaleIobAmountForChart<T: Numeric & Comparable>(_ rawAmount: T) -> T where T: ExpressibleByIntegerLiteral {
+        rawAmount > 0 ? rawAmount * 8 : rawAmount * 9
+    }
+
     func drawCOBIOBChart() -> some ChartContent {
         // Filter out duplicate entries by `deliverAt`,
         // We sometimes get two determinations when editing carbs, one without the entry-to-be-edited and then another one after editing the entry.
@@ -115,9 +128,7 @@ extension MainChartView {
             // MARK: - IOB line and area mark
 
             let rawAmount = item.iob?.doubleValue ?? 0
-
-            // as iob and cob share the same y axis and cob is usually >> iob we need to weigh iob visually
-            let amountIOB: Double = rawAmount > 0 ? rawAmount * 8 : rawAmount * 9
+            let amountIOB: Double = scaleIobAmountForChart(rawAmount)
 
             AreaMark(x: .value("Time", date), y: .value("Amount", amountIOB))
                 .foregroundStyle(by: .value("Type", "IOB"))

+ 33 - 5
Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift

@@ -12,6 +12,7 @@ struct LoopStatusView: View {
     @State var helpSheetDetent = PresentationDetent.fraction(0.9)
 
     @State private var statusTitle: String = ""
+    @State private var lastDetermination: OrefDetermination?
 
     var body: some View {
         ScrollView {
@@ -44,7 +45,8 @@ struct LoopStatusView: View {
 
                 if let errorMessage = state.errorMessage, let date = state.errorDate {
                     Group {
-                        Text("Error During Algorithm Run at \(Formatter.dateFormatter.string(from: date))").font(.headline)
+                        Text("Loop at \(Formatter.dateFormatter.string(from: date)) failed.").font(.headline)
+                            .font(.headline)
                             .fixedSize(horizontal: false, vertical: true)
                         Text(errorMessage).font(.caption).fixedSize(horizontal: false, vertical: true)
                     }.foregroundColor(.loopRed)
@@ -124,6 +126,9 @@ struct LoopStatusView: View {
                 LoopStatusHelpView(state: state, helpSheetDetent: $helpSheetDetent, isHelpSheetPresented: $isHelpSheetPresented)
             }
         }
+        .onAppear {
+            lastDetermination = state.determinationsFromPersistence.first
+        }
         .presentationDetents([.height(sheetContentHeight)])
         .presentationDragIndicator(.visible)
         .onPreferenceChange(ContentSizeKey.self) { newSize in
@@ -163,11 +168,34 @@ struct LoopStatusView: View {
     }
 
     private func setStatusTitle() {
-        if let determination = state.determinationsFromPersistence.first {
-            statusTitle =
-                "Enacted at \(Formatter.dateFormatter.string(from: determination.deliverAt ?? Date()))"
+        if let determination = state.determinationsFromPersistence.first, let deliverAt = determination.deliverAt {
+            let minutesAgo = abs(deliverAt.timeIntervalSinceNow) / 60
+
+            if deliverAt < Date().addingTimeInterval(-5 * 60) {
+                let roundedMinutes = Int(minutesAgo.rounded())
+                statusTitle = String(
+                    localized: "Trio has not looped in \(roundedMinutes) minutes."
+                )
+            } else {
+                statusTitle = String(
+                    localized: "Enacted at \(Formatter.dateFormatter.string(from: deliverAt))"
+                )
+            }
+        } else if let determination = lastDetermination, let deliverAt = determination.deliverAt {
+            let minutesAgo = abs(deliverAt.timeIntervalSinceNow) / 60
+
+            if deliverAt < Date().addingTimeInterval(-5 * 60) {
+                let roundedMinutes = Int(minutesAgo.rounded())
+                statusTitle = String(
+                    localized: "Trio has not looped in \(roundedMinutes) minutes."
+                )
+            } else {
+                statusTitle = String(
+                    localized: "Enacted at \(Formatter.dateFormatter.string(from: deliverAt))"
+                )
+            }
         } else {
-            statusTitle = "Not enacted."
+            statusTitle = String(localized: "Not looping.")
         }
     }
 

+ 32 - 23
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -460,32 +460,41 @@ extension Home {
 
                 Spacer()
 
-                HStack {
-                    if state.pumpSuspended {
-                        Text("Pump suspended")
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                            .foregroundColor(.loopGray)
-                    } else if let tempBasalString = tempBasalString {
-                        Image(systemName: "drop.circle")
-                            .font(.callout)
-                            .foregroundColor(.insulinTintColor)
-                        if tempBasalString.count > 5 {
-                            Text(tempBasalString)
+                if state.maxIOB == 0.0 {
+                    HStack {
+                        Image(systemName: "exclamationmark.circle.fill")
+                        Text("MaxIOB: 0 U")
+                    }.bold()
+                        .foregroundStyle(Color.red)
+                        .font(.callout)
+                } else {
+                    HStack {
+                        if state.pumpSuspended {
+                            Text("Pump suspended")
                                 .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                                .lineLimit(1)
-                                .minimumScaleFactor(0.85)
-                                .truncationMode(.tail)
-                                .allowsTightening(true)
+                                .foregroundColor(.loopGray)
+                        } else if let tempBasalString = tempBasalString {
+                            Image(systemName: "drop.circle")
+                                .font(.callout)
+                                .foregroundColor(.insulinTintColor)
+                            if tempBasalString.count > 5 {
+                                Text(tempBasalString)
+                                    .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                    .lineLimit(1)
+                                    .minimumScaleFactor(0.85)
+                                    .truncationMode(.tail)
+                                    .allowsTightening(true)
+                            } else {
+                                // Short strings can just display normally
+                                Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            }
                         } else {
-                            // Short strings can just display normally
-                            Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            Image(systemName: "drop.circle")
+                                .font(.callout)
+                                .foregroundColor(.insulinTintColor)
+                            Text("No Data")
+                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                         }
-                    } else {
-                        Image(systemName: "drop.circle")
-                            .font(.callout)
-                            .foregroundColor(.insulinTintColor)
-                        Text("No Data")
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
                 }
             }.padding(.horizontal)

+ 6 - 6
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -123,6 +123,9 @@ extension NightscoutConfig {
                                         }
                                     }
                                 ).buttonStyle(BorderlessButtonStyle())
+                                    .alert(isPresented: $isImportAlertPresented) {
+                                        importAlert ?? Alert(title: Text("Unknown Error"))
+                                    }
                             }.padding(.top)
                         }.padding(.vertical)
                     }.listRowBackground(Color.chart)
@@ -177,6 +180,9 @@ extension NightscoutConfig {
                                             }
                                         }
                                     ).buttonStyle(BorderlessButtonStyle())
+                                        .alert(isPresented: $isBackfillAlertPresented) {
+                                            backfillAlert ?? Alert(title: Text("Unknown Error"))
+                                        }
                                 }.padding(.top)
                             }.padding(.vertical)
                         }
@@ -206,12 +212,6 @@ extension NightscoutConfig {
             }
             .navigationBarTitle("Nightscout")
             .navigationBarTitleDisplayMode(.automatic)
-            .alert(isPresented: $isImportAlertPresented) {
-                importAlert ?? Alert(title: Text("Unknown Error"))
-            }
-            .alert(isPresented: $isBackfillAlertPresented) {
-                backfillAlert ?? Alert(title: Text("Unknown Error"))
-            }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
         }

+ 0 - 4
Trio/Sources/Modules/SMBSettings/SMBSettingsStateModel.swift

@@ -17,8 +17,6 @@ extension SMBSettings {
         @Published var enableSMB_high_bg: Bool = false
         @Published var enableSMB_high_bg_target: Decimal = 100
         @Published var maxSMBBasalMinutes: Decimal = 30
-        @Published var smbDeliveryRatio: Decimal = 0.5
-        @Published var smbInterval: Decimal = 3
         @Published var bolusIncrement: Decimal = 0.1 // get this from pump, dafuq?: Bool = false
         @Published var enableUAM: Bool = false
         @Published var maxUAMSMBBasalMinutes: Decimal = 30
@@ -37,8 +35,6 @@ extension SMBSettings {
             subscribePreferencesSetting(\.enableSMB_high_bg_target, on: $enableSMB_high_bg_target) {
                 enableSMB_high_bg_target = $0 }
             subscribePreferencesSetting(\.maxSMBBasalMinutes, on: $maxSMBBasalMinutes) { maxSMBBasalMinutes = $0 }
-            subscribePreferencesSetting(\.smbDeliveryRatio, on: $smbDeliveryRatio) { smbDeliveryRatio = $0 }
-            subscribePreferencesSetting(\.smbInterval, on: $smbInterval) { smbInterval = $0 }
             subscribePreferencesSetting(\.bolusIncrement, on: $bolusIncrement) { bolusIncrement = $0 }
             subscribePreferencesSetting(\.enableUAM, on: $enableUAM) { enableUAM = $0 }
             subscribePreferencesSetting(\.maxUAMSMBBasalMinutes, on: $maxUAMSMBBasalMinutes) { maxUAMSMBBasalMinutes = $0 }

+ 2 - 54
Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift

@@ -262,7 +262,7 @@ extension SMBSettings {
                             Text(
                                 "𝒳 = Max SMB Basal Minutes"
                             )
-                            Text("(𝒳 ÷ 60) × current basal rate")
+                            Text("(𝒳 / 60) × current basal rate")
                         }
 
                         VStack(alignment: .leading, spacing: 10) {
@@ -308,7 +308,7 @@ extension SMBSettings {
                             Text(
                                 "𝒳 = Max UAM SMB Basal Minutes"
                             )
-                            Text("(𝒳 ÷ 60) × current basal rate")
+                            Text("(𝒳 / 60) × current basal rate")
                         }
                         VStack(alignment: .leading, spacing: 10) {
                             Text(
@@ -343,58 +343,6 @@ extension SMBSettings {
                         Text("Note: This setting has a hard-coded cap of 40%")
                     }
                 )
-
-                SettingInputSection(
-                    decimalValue: $state.smbDeliveryRatio,
-                    booleanValue: $booleanPlaceholder,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "SMB Delivery Ratio", comment: "SMB Delivery Ratio")
-                        }
-                    ),
-                    units: state.units,
-                    type: .decimal("smbDeliveryRatio"),
-                    label: String(localized: "SMB Delivery Ratio", comment: "SMB Delivery Ratio"),
-                    miniHint: String(localized: "Percentage of calculated insulin required that is given as SMB."),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: 50%").bold()
-                        Text(
-                            "Once the total insulin required is calculated, this safety limit specifies what percentage of the insulin required can be delivered as an SMB."
-                        )
-                        Text(
-                            "Due to SMBs potentially occurring every 5 minutes with each loop cycle, it is important to set this value to a reasonable level that allows Trio to safely zero temp should dosing needs suddenly change. Increase this value with caution."
-                        )
-                        Text("Note: Allowed range is 30 - 70%")
-                    }
-                )
-
-                SettingInputSection(
-                    decimalValue: $state.smbInterval,
-                    booleanValue: $booleanPlaceholder,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "SMB Interval", comment: "SMB Interval")
-                        }
-                    ),
-                    units: state.units,
-                    type: .decimal("smbInterval"),
-                    label: String(localized: "SMB Interval", comment: "SMB Interval"),
-                    miniHint: String(localized: "Minimum minutes since the last SMB or manual bolus to allow an automated SMB."),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: 3 min").bold()
-                        Text(
-                            "This is the minimum number of minutes since the last SMB or manual bolus before Trio will permit an automated SMB."
-                        )
-                    }
-                )
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {

+ 41 - 47
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -4,16 +4,16 @@ import SwiftUI
 
 struct SettingItem: Identifiable {
     let id = UUID()
-    let title: LocalizedStringKey
+    let title: String
     let view: Screen
-    let searchContents: [LocalizedStringKey]?
-    let path: [LocalizedStringKey]?
+    let searchContents: [String]?
+    let path: [String]?
 
     init(
-        title: LocalizedStringKey,
+        title: String,
         view: Screen,
-        searchContents: [LocalizedStringKey]? = nil,
-        path: [LocalizedStringKey]? = nil
+        searchContents: [String]? = nil,
+        path: [String]? = nil
     ) {
         self.title = title
         self.view = view
@@ -25,7 +25,7 @@ struct SettingItem: Identifiable {
 struct FilteredSettingItem: Identifiable {
     let id = UUID()
     let settingItem: SettingItem
-    let matchedContent: LocalizedStringKey
+    let matchedContent: String
 }
 
 enum SettingItems {
@@ -65,7 +65,7 @@ enum SettingItems {
         SettingItem(
             title: "Units and Limits",
             view: .unitsAndLimits,
-            searchContents: ["Glucose Units", "Max Basal", "Max Bolus", "Max IOB", "Max COB"],
+            searchContents: ["Glucose Units", "Max Basal", "Max Bolus", "Max IOB", "Max COB", "Minimum Safety Threshold"],
             path: ["Therapy Settings", "Units and Limits"]
         ),
         SettingItem(title: "Basal Rates", view: .basalProfileEditor, path: ["Therapy Settings"]),
@@ -97,9 +97,7 @@ enum SettingItems {
                 "Enable UAM",
                 "Max SMB Basal Minutes",
                 "Max UAM SMB Basal Minutes",
-                "Max Delta-BG Threshold SMB",
-                "SMB Delivery Ratio",
-                "SMB Interval"
+                "Max Delta-BG Threshold SMB"
             ],
             path: ["Algorithm", "Super Micro Bolus (SMB)"]
         ),
@@ -114,8 +112,7 @@ enum SettingItems {
                 "AF",
                 "Sigmoid Adjustment Factor",
                 "Weighted Average of TDD",
-                "Adjust Basal",
-                "Minimum Safety Threshold"
+                "Adjust Basal"
             ],
             path: ["Algorithm", "Dynamic Sensitivity"]
         ),
@@ -143,6 +140,8 @@ enum SettingItems {
                 "Skip Neutral Temps",
                 "Unsuspend If No Temp",
                 "Suspend Zeros IOB",
+                "SMB Delivery Ratio",
+                "SMB Interval",
                 "Min 5m Carbimpact",
                 "Remaining Carbs Fraction",
                 "Remaining Carbs Cap",
@@ -162,7 +161,8 @@ enum SettingItems {
                 "Enable Fatty Meal Factor",
                 "Fatty Meal Factor",
                 "Enable Super Bolus",
-                "Super Bolus Factor"
+                "Super Bolus Factor",
+                "Very Low Glucose Warning"
             ],
             path: ["Features", "Bolus Calculator"]
         ),
@@ -205,8 +205,7 @@ enum SettingItems {
                 "Low Threshold",
                 "High Threshold",
                 "X-Axis Interval Step",
-                "Override eA1c Unit",
-                "Standing / Laying TIR Chart",
+                "eA1c/GMI Display Unit",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
                 "Forecast Display Type",
@@ -217,7 +216,10 @@ enum SettingItems {
                 "Appearance",
                 "Dark Scheme",
                 "Light Scheme",
-                "Glucose Color Scheme"
+                "Glucose Color Scheme",
+                "Time in Range Type",
+                "Time in Tight Range (TITR)",
+                "Time in Normoglycemia (TING)"
             ],
             path: ["Features", "User Interface"]
         ),
@@ -302,19 +304,23 @@ enum SettingItems {
     static func filteredItems(searchText: String) -> [FilteredSettingItem] {
         allItems.flatMap { item in
             var results = [FilteredSettingItem]()
-            let searchTextToLower = searchText.lowercased()
+            let searchLower = searchText.lowercased()
+
+            let titleLocalized = item.title.localized
+            let titleEnglish = item.title.englishLocalized
 
-            if item.title.stringValue.localizedCaseInsensitiveContains(searchTextToLower) ||
-                item.title.englishValue.localizedCaseInsensitiveContains(searchTextToLower)
+            if titleLocalized.localizedCaseInsensitiveContains(searchLower) ||
+                titleEnglish.localizedCaseInsensitiveContains(searchLower)
             {
                 results.append(FilteredSettingItem(settingItem: item, matchedContent: item.title))
             }
 
-            if let matchedContents = item.searchContents?.filter({
-                $0.stringValue.localizedCaseInsensitiveContains(searchTextToLower) ||
-                    $0.englishValue.localizedCaseInsensitiveContains(searchTextToLower)
-            }) {
-                results.append(contentsOf: matchedContents.map { FilteredSettingItem(settingItem: item, matchedContent: $0) })
+            if let contents = item.searchContents {
+                let matched = contents.filter {
+                    $0.localized.localizedCaseInsensitiveContains(searchLower) ||
+                        $0.englishLocalized.localizedCaseInsensitiveContains(searchLower)
+                }
+                results.append(contentsOf: matched.map { FilteredSettingItem(settingItem: item, matchedContent: $0) })
             }
 
             return results
@@ -322,29 +328,17 @@ enum SettingItems {
     }
 }
 
-extension LocalizedStringKey {
-    var stringValue: String {
-        let mirror = Mirror(reflecting: self)
-        let children = mirror.children
-        if let label = children.first(where: { $0.label == "key" })?.value as? String {
-            return String(localized: "\(label)", comment: "")
-        } else {
-            return ""
+extension String {
+    func localizedString(locale: Locale = .current) -> String {
+        if locale.identifier == "en",
+           let path = Bundle.main.path(forResource: "en", ofType: "lproj"),
+           let bundle = Bundle(path: path)
+        {
+            return NSLocalizedString(self, bundle: bundle, comment: "")
         }
+        return NSLocalizedString(self, comment: "")
     }
 
-    var englishValue: String {
-        let mirror = Mirror(reflecting: self)
-        let children = mirror.children
-
-        if let key = children.first(where: { $0.label == "key" })?.value as? String {
-            if let path = Bundle.main.path(forResource: "en", ofType: "lproj"),
-               let bundle = Bundle(path: path)
-            {
-                return bundle.localizedString(forKey: key, value: nil, table: nil)
-            }
-        }
-
-        return ""
-    }
+    var localized: String { localizedString() }
+    var englishLocalized: String { localizedString(locale: Locale(identifier: "en")) }
 }

+ 7 - 1
Trio/Sources/Modules/Settings/SettingsStateModel.swift

@@ -1,6 +1,7 @@
 import LoopKit
 import LoopKitUI
 import SwiftUI
+import TidepoolServiceKit
 
 extension Settings {
     final class StateModel: BaseStateModel<Provider> {
@@ -37,7 +38,7 @@ extension Settings {
 
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
 
-            serviceUIType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
+            serviceUIType = TidepoolService.self as? ServiceUI.Type
         }
 
         func logItems() -> [URL] {
@@ -74,6 +75,11 @@ extension Settings {
 //            let storageURL = localDocuments.appendingPathComponent("PumpManagerState" + ".plist")
 //            try? FileManager.default.removeItem(at: storageURL)
 //        }
+        func hasCgmAndPump() -> Bool {
+            let hasCgm = fetchCgmManager.cgmGlucoseSourceType != .none
+            let hasPump = provider.deviceManager.pumpManager != nil
+            return hasCgm && hasPump
+        }
     }
 }
 

+ 17 - 5
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -29,6 +29,7 @@ extension Settings {
             isUpdateAvailable: false,
             isBlacklisted: false
         )
+        @State private var closedLoopDisabled = true
 
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
@@ -113,6 +114,11 @@ extension Settings {
                         }
                     ).listRowBackground(Color.chart)
 
+                    let miniHintText = closedLoopDisabled ?
+                        String(localized: "Add a CGM and pump to enable automated insuin delivery") :
+                        String(localized: "Enable automated insulin delivery.")
+                    let miniHintTextColorForDisabled: Color = colorScheme == .dark ? .orange : .accentColor
+                    let miniHintTextColor: Color = closedLoopDisabled ? miniHintTextColorForDisabled : .secondary
                     SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.closedLoop,
@@ -127,7 +133,7 @@ extension Settings {
                         units: state.units,
                         type: .boolean,
                         label: String(localized: "Closed Loop"),
-                        miniHint: String(localized: "Enable automated insulin delivery."),
+                        miniHint: miniHintText,
                         verboseHint: VStack(alignment: .leading, spacing: 10) {
                             Text(
                                 "Running Trio in closed loop mode requires an active CGM sensor session and a connected pump. This enables automated insulin delivery."
@@ -136,14 +142,19 @@ extension Settings {
                                 "Before enabling, dial in your settings (basal / insulin sensitivity / carb ratio), and familiarize yourself with the app."
                             )
                         },
-                        headerText: String(localized: "Automated Insulin Delivery")
+                        headerText: String(localized: "Automated Insulin Delivery"),
+                        isToggleDisabled: closedLoopDisabled,
+                        miniHintColor: miniHintTextColor
                     )
+                    .onAppear {
+                        closedLoopDisabled = !state.hasCgmAndPump()
+                    }
 
                     Section(
                         header: Text("Trio Configuration"),
                         content: {
                             ForEach(SettingItems.trioConfig) { item in
-                                Text(item.title).navigationLink(to: item.view, from: self)
+                                Text(LocalizedStringKey(item.title)).navigationLink(to: item.view, from: self)
                             }
                         }
                     )
@@ -239,12 +250,13 @@ extension Settings {
                             if filteredItems.isNotEmpty {
                                 ForEach(filteredItems) { filteredItem in
                                     VStack(alignment: .leading) {
-                                        Text(filteredItem.matchedContent).bold()
+                                        Text(filteredItem.matchedContent.localized).bold()
                                         if let path = filteredItem.settingItem.path {
-                                            Text(path.map(\.stringValue).joined(separator: " > "))
+                                            Text(path.map(\.localized).joined(separator: " > "))
                                                 .font(.caption)
                                                 .foregroundColor(.secondary)
                                         }
+
                                     }.navigationLink(to: filteredItem.settingItem.view, from: self)
                                 }
                             } else {

+ 4 - 1
Trio/Sources/Modules/Settings/View/Subviews/SubmodulesView.swift

@@ -5,7 +5,10 @@ struct SubmodulesView: View {
 
     var body: some View {
         List {
-            Section {
+            Section(header: Text("Trio")) {
+                KeyValueRow(key: buildDetails.trioBranch, value: buildDetails.trioCommitSHA)
+            }
+            Section(header: Text("Submodules")) {
                 ForEach(buildDetails.submodules.sorted(by: { $0.key < $1.key }), id: \.key) { name, info in
                     KeyValueRow(key: name, value: info.commitSHA)
                 }

+ 25 - 13
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -23,6 +23,16 @@ struct LoopStatsByPeriod: Identifiable {
     var id: Date { period }
 }
 
+struct LoopStatsProcessedData: Identifiable {
+    var id = UUID()
+    let category: LoopStatsDataType
+    let count: Int
+    let percentage: Double
+    let medianDuration: Double
+    let medianInterval: Double
+    let totalDays: Int
+}
+
 enum LoopStatsDataType: String {
     case successfulLoop
     case glucoseCount
@@ -142,7 +152,7 @@ extension Stat.StateModel {
         failedLoopIds: [NSManagedObjectID],
         interval: StatsTimeIntervalWithToday
     ) async throws
-        -> [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+        -> [LoopStatsProcessedData]
     {
         // Calculate the date range for glucose readings
         let now = Date()
@@ -197,19 +207,21 @@ extension Stat.StateModel {
             let glucosePercentage = (averageGlucosePerDay / maxLoopsPerDay) * 100
 
             return [
-                (
-                    LoopStatsDataType.successfulLoop,
-                    Int(round(averageLoopsPerDay)),
-                    loopPercentage,
-                    medianDuration,
-                    medianInterval
+                LoopStatsProcessedData(
+                    category: LoopStatsDataType.successfulLoop,
+                    count: Int(round(averageLoopsPerDay)),
+                    percentage: loopPercentage,
+                    medianDuration: medianDuration,
+                    medianInterval: medianInterval,
+                    totalDays: numberOfDays
                 ),
-                (
-                    LoopStatsDataType.glucoseCount,
-                    Int(round(averageGlucosePerDay)),
-                    glucosePercentage,
-                    medianDuration,
-                    medianInterval
+                LoopStatsProcessedData(
+                    category: LoopStatsDataType.glucoseCount,
+                    count: Int(round(averageGlucosePerDay)),
+                    percentage: glucosePercentage,
+                    medianDuration: medianDuration,
+                    medianInterval: medianInterval,
+                    totalDays: numberOfDays
                 )
             ]
         }

+ 6 - 3
Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift

@@ -93,9 +93,12 @@ extension Stat.StateModel {
             // Ranges are processed from bottom to top in the stacked chart
             let ranges: [(name: String, condition: (Int) -> Bool)] = [
                 ("<54", { g in g <= 54 }),
-                ("54-70", { g in g > 54 && g < 70 }),
-                ("70-140", { g in g >= 70 && g <= 140 }),
-                ("140-180", { g in g > 140 && g <= 180 }),
+                ("54-\(self.timeInRangeType.bottomThreshold)", { g in g > 54 && g < self.timeInRangeType.bottomThreshold }),
+                (
+                    "\(self.timeInRangeType.bottomThreshold)-\(self.timeInRangeType.topThreshold)",
+                    { g in g >= self.timeInRangeType.bottomThreshold && g <= self.timeInRangeType.topThreshold }
+                ),
+                ("\(self.timeInRangeType.topThreshold)-180", { g in g > self.timeInRangeType.topThreshold && g <= 180 }),
                 ("180-200", { g in g > 180 && g <= 200 }),
                 ("200-220", { g in g > 200 && g <= 220 }),
                 (">220", { g in g > 220 })

+ 3 - 9
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -10,18 +10,12 @@ extension Stat {
         var highLimit: Decimal = 180
         var lowLimit: Decimal = 70
         var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
-        var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
         var units: GlucoseUnits = .mgdL
+        var timeInRangeType: TimeInRangeType = .timeInTightRange
         var useFPUconversion: Bool = false
         var glucoseFromPersistence: [GlucoseStored] = []
         var loopStatRecords: [LoopStatRecord] = []
-        var loopStats: [(
-            category: LoopStatsDataType,
-            count: Int,
-            percentage: Double,
-            medianDuration: Double,
-            medianInterval: Double
-        )] = []
+        var loopStats: [LoopStatsProcessedData] = []
         var groupedLoopStats: [LoopStatsByPeriod] = []
         var bolusStats: [BolusStats] = []
         var hourlyStats: [HourlyStats] = []
@@ -91,8 +85,8 @@ extension Stat {
             setupMealStats()
             units = settingsManager.settings.units
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
-            timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
             useFPUconversion = settingsManager.settings.useFPUconversion
+            timeInRangeType = settingsManager.settings.timeInRangeType
         }
 
         func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {

+ 4 - 2
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -130,7 +130,8 @@ extension Stat {
                             highLimit: state.highLimit,
                             lowLimit: state.lowLimit,
                             units: state.units,
-                            glucoseRangeStats: state.glucoseRangeStats
+                            glucoseRangeStats: state.glucoseRangeStats,
+                            timeInRangeType: state.timeInRangeType
                         )
                     }
                 }
@@ -144,7 +145,8 @@ extension Stat {
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        glucose: state.glucoseFromPersistence
+                        glucose: state.glucoseFromPersistence,
+                        timeInRangeType: state.timeInRangeType
                     )
 
                     Divider()

+ 10 - 6
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift

@@ -7,6 +7,7 @@ struct GlucoseDistributionChart: View {
     let lowLimit: Decimal
     let units: GlucoseUnits
     let glucoseRangeStats: [GlucoseRangeStats]
+    let timeInRangeType: TimeInRangeType
 
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
@@ -25,9 +26,9 @@ struct GlucoseDistributionChart: View {
             }
             .chartForegroundStyleScale([
                 "<54": .purple.opacity(0.7),
-                "54-70": .red.opacity(0.7),
-                "70-140": .green,
-                "140-180": .green.opacity(0.7),
+                "54-\(timeInRangeType.bottomThreshold)": .red.opacity(0.7),
+                "\(timeInRangeType.bottomThreshold)-\(timeInRangeType.topThreshold)": .green,
+                "\(timeInRangeType.topThreshold)-180": .green.opacity(0.7),
                 "180-200": .yellow.opacity(0.7),
                 "200-220": .orange.opacity(0.7),
                 ">220": .orange.opacity(0.8)
@@ -36,12 +37,15 @@ struct GlucoseDistributionChart: View {
                 let legendItems: [(String, Color)] = [
                     ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.7)),
                     (
-                        "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(70) : 70.asMmolL)",
+                        "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)",
                         .red.opacity(0.7)
                     ),
-                    ("\(units == .mgdL ? Decimal(70) : 70.asMmolL)-\(units == .mgdL ? Decimal(140) : 140.asMmolL)", .green),
                     (
-                        "\(units == .mgdL ? Decimal(140) : 140.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
+                        "\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)",
+                        .green
+                    ),
+                    (
+                        "\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
                         .green.opacity(0.7)
                     ),
                     (

+ 20 - 12
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift

@@ -27,11 +27,13 @@ struct GlucoseMetricsView: View {
         let totalDays = (latestDate - earliestDate).timeInterval / 86400
 
         // Format glucose statistics based on the selected unit
-        let eA1cString = preferredUnit == .mmolL
-            ? glucoseStats.ifcc.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-            : glucoseStats.ngsp.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+        let eA1cString = preferredUnit == .mgdL
+            ? (glucoseStats.ngsp.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%") : glucoseStats
+            .ifcc.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
 
-        let gmiString = glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+        let gmiString = preferredUnit == .mgdL ?
+            (glucoseStats.gmiPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%") :
+            glucoseStats.gmiMmolMol.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
 
         // glucoseStats already parsed to units - only format decimals
         let standardDeviationString = units == .mgdL ? glucoseStats.sd.formatted(
@@ -40,7 +42,7 @@ struct GlucoseMetricsView: View {
             .number.grouping(.never).rounded().precision(.fractionLength(1))
         )
         let coefficientOfVariationString = glucoseStats.cv
-            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
+            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
         let daysTrackedString = totalDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
 
         VStack(alignment: .leading) {
@@ -60,14 +62,14 @@ struct GlucoseMetricsView: View {
 
     /// Computes various statistical metrics from stored glucose readings, including:
     /// - Estimated A1c in NGSP (%) and IFCC (mmol/mol)
-    /// - Glucose Management Index (GMI)
+    /// - Glucose Management Index (GMI) in both mmol/mol and percentage
     /// - Average and median glucose levels
     /// - Standard deviation (SD) and coefficient of variation (CV)
     /// - Number of readings per day
     ///
     /// - Returns: A tuple containing glucose statistics.
     func calculateGlucoseStatistics() -> (
-        ifcc: Double, ngsp: Double, gmi: Double, average: Double,
+        ifcc: Double, ngsp: Double, gmiMmolMol: Double, gmiPercentage: Double, average: Double,
         median: Double, sd: Double, cv: Double, readingsPerDay: Double
     ) {
         // Determine the date range of the glucose data
@@ -84,7 +86,7 @@ struct GlucoseMetricsView: View {
 
         // Handle empty dataset case
         guard totalReadings > 1 else {
-            return (ifcc: 0, ngsp: 0, gmi: 0, average: 0, median: 0, sd: 0, cv: 0, readingsPerDay: 0)
+            return (ifcc: 0, ngsp: 0, gmiMmolMol: 0, gmiPercentage: 0, average: 0, median: 0, sd: 0, cv: 0, readingsPerDay: 0)
         }
 
         let sumOfReadings = glucoseValues.reduce(0, +)
@@ -96,7 +98,8 @@ struct GlucoseMetricsView: View {
         // Estimated A1c and Glucose Management Index (GMI) calculations
         var eA1cNGSP = 0.0 // eA1c NGSP (%)
         var eA1cIFCC = 0.0 // eA1c IFCC (mmol/mol)
-        var gmiValue = 0.0 // Glucose Management Index (GMI)
+        var gmiValuePercentage = 0.0 // GMI (%)
+        var gmiValueMmolMol = 0.0 // GMI (mmol/mol)
 
         if totalDays > 0 {
             // **eA1c NGSP Calculation** (CGM-based)
@@ -107,9 +110,13 @@ struct GlucoseMetricsView: View {
             // eA1c IFCC (mmol/mol) = 10.929 * (eA1c NGSP - 2.152)
             eA1cIFCC = 10.929 * (eA1cNGSP - 2.152)
 
-            // **Glucose Management Index (GMI)**
+            // **Glucose Management Index (GMI) in %**
             // GMI = 3.31 + (0.02392 × Average Glucose mg/dL)
-            gmiValue = 3.31 + (0.02392 * meanGlucose)
+            gmiValuePercentage = 3.31 + (0.02392 * meanGlucose)
+
+            // **Glucose Management Index (GMI) in mmol/mol**
+            // GMI mmol/mol = (GMI % - 2.15) * 10.929
+            gmiValueMmolMol = (gmiValuePercentage - 2.152) * 10.929
         }
 
         // Compute Standard Deviation (SD) and Coefficient of Variation (CV)
@@ -123,7 +130,8 @@ struct GlucoseMetricsView: View {
         return (
             ifcc: eA1cIFCC, // eA1c in IFCC (mmol/mol)
             ngsp: eA1cNGSP, // eA1c in NGSP (%)
-            gmi: gmiValue, // Glucose Management Index
+            gmiMmolMol: gmiValueMmolMol, // GMI in mmol/mol
+            gmiPercentage: gmiValuePercentage, // GMI in %
             average: Double(units == .mgdL ? Decimal(meanGlucose) : meanGlucose.asMmolL),
             median: Double(units == .mgdL ? Decimal(medianGlucose) : medianGlucose.asMmolL),
             sd: Double(units == .mgdL ? Decimal(standardDeviation) : standardDeviation.asMmolL),

+ 14 - 5
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift

@@ -8,6 +8,7 @@ struct GlucoseSectorChart: View {
     let lowLimit: Decimal
     let units: GlucoseUnits
     let glucose: [GlucoseStored]
+    let timeInRangeType: TimeInRangeType
 
     @State private var selectedCount: Int?
     @State private var selectedRange: GlucoseRange?
@@ -28,8 +29,9 @@ struct GlucoseSectorChart: View {
             let total = Decimal(glucose.count)
             // Count readings between high limit and 250 mg/dL (high)
             let high = glucose.filter { $0.glucose > Int(highLimit) }.count
-            // Count readings between low limit and 140 mg/dL (tight control)
-            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            // Count readings between low limit (TITR: 70 mg/dL, TING 63 mg/dL) and 140 mg/dL (tight control)
+            let tight = glucose
+                .filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= timeInRangeType.topThreshold }.count
             // Count readings between 140 and high limit (normal range)
             let normal = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }.count
             // Count readings between 54 and low limit (low)
@@ -54,7 +56,11 @@ struct GlucoseSectorChart: View {
                 }
 
                 VStack(alignment: .leading, spacing: 5) {
-                    Text("\(formatValue(lowLimit))-\(formatValue(140))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(
+                        "\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold)))"
+                    )
+                    .font(.subheadline)
+                    .foregroundStyle(Color.secondary)
                     Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
                         .foregroundStyle(Color.green)
                 }
@@ -219,7 +225,8 @@ struct GlucoseSectorChart: View {
             )
 
         case .inRange:
-            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            let tight = glucose
+                .filter { $0.glucose >= Int(timeInRangeType.bottomThreshold) && $0.glucose <= timeInRangeType.topThreshold }.count
             let glucoseValues = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }
             let glucoseValuesAsInt = glucoseValues.map { Int($0.glucose) }
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: glucoseValuesAsInt)
@@ -233,7 +240,9 @@ struct GlucoseSectorChart: View {
                         formatPercentage(Decimal(glucoseValues.count) / total * 100)
                     ),
                     (
-                        String(localized: "Tight (\(formatValue(lowLimit))-\(formatValue(140)))"),
+                        String(
+                            localized: "\(timeInRangeType == .timeInTightRange ? "TITR" : "TING") (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold))))"
+                        ),
                         formatPercentage(Decimal(tight) / total * 100)
                     ),
                     (String(localized: "Average"), formatValue(average)),

+ 2 - 8
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift

@@ -4,7 +4,7 @@ import SwiftUI
 struct LoopBarChartView: View {
     let loopStatRecords: [LoopStatRecord]
     let selectedInterval: Stat.StateModel.StatsTimeIntervalWithToday
-    let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+    let statsData: [LoopStatsProcessedData]
 
     var body: some View {
         VStack(spacing: 20) {
@@ -50,13 +50,7 @@ struct LoopBarChartView: View {
         }
     }
 
-    private func annotationText(for data: (
-        category: LoopStatsDataType,
-        count: Int,
-        percentage: Double,
-        medianDuration: Double,
-        medianInterval: Double
-    )) -> String {
+    private func annotationText(for data: LoopStatsProcessedData) -> String {
         if data.category == .successfulLoop {
             switch selectedInterval {
             case .day,

+ 6 - 1
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift

@@ -4,7 +4,7 @@ import SwiftUI
 /// A SwiftUI view displaying statistics about the looping process in an Automated Insulin Delivery (AID) system.
 struct LoopStatsView: View {
     /// The list of loop statistics records used to generate the statistics.
-    let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+    let statsData: [LoopStatsProcessedData]
 
     /// The main body of the `LoopStatsView`, displaying loop statistics.
     var body: some View {
@@ -32,6 +32,11 @@ struct LoopStatsView: View {
                     value: (successfulStats.percentage / 100)
                         .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
                 )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Days"),
+                    value: successfulStats.totalDays.description
+                )
             }
             .padding()
         }

+ 0 - 58
Trio/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -1,58 +0,0 @@
-import SwiftUI
-
-extension StatConfig {
-    final class StateModel: BaseStateModel<Provider> {
-        @Published var overrideHbA1cUnit = false
-
-        @Published var skipBolusScreenAfterCarbs: Bool = false
-        @Published var useFPUconversion: Bool = true
-        @Published var tins: Bool = false
-        @Published var historyLayout: HistoryLayout = .twoTabs
-        @Published var lockScreenView: LockScreenView = .simple
-        @Published var low: Decimal = 70
-        @Published var high: Decimal = 180
-        @Published var hours: Decimal = 6
-        @Published var dynamicGlucoseColor = false
-        @Published var xGridLines = false
-        @Published var yGridLines: Bool = false
-        @Published var oneDimensionalGraph = false
-        @Published var rulerMarks: Bool = true
-        @Published var displayForecastsAsLines: Bool = false
-
-        var units: GlucoseUnits = .mgdL
-
-        override func subscribe() {
-            let units = settingsManager.settings.units
-            self.units = units
-
-            subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
-            subscribeSetting(\.dynamicGlucoseColor, on: $dynamicGlucoseColor) { dynamicGlucoseColor = $0 }
-            subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
-            subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
-            subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
-            subscribeSetting(\.displayForecastsAsLines, on: $displayForecastsAsLines) { displayForecastsAsLines = $0 }
-            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
-            subscribeSetting(\.tins, on: $tins) { tins = $0 }
-            subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
-            subscribeSetting(\.oneDimensionalGraph, on: $oneDimensionalGraph) { oneDimensionalGraph = $0 }
-            subscribeSetting(\.historyLayout, on: $historyLayout) { historyLayout = $0 }
-            subscribeSetting(\.lockScreenView, on: $lockScreenView) { lockScreenView = $0 }
-
-            subscribeSetting(\.low, on: $low, initial: {
-                let value = max(min($0, 90), 40)
-                low = units == .mmolL ? value.asMmolL : value
-            }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
-            })
-
-            subscribeSetting(\.high, on: $high, initial: {
-                let value = max(min($0, 270), 110)
-                high = units == .mmolL ? value.asMmolL : value
-            }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
-            })
-        }
-    }
-}

+ 0 - 113
Trio/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -1,113 +0,0 @@
-import SwiftUI
-import Swinject
-
-extension StatConfig {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        @StateObject var state = StateModel()
-
-        @Environment(\.colorScheme) var colorScheme
-        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 var glucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            if state.units == .mmolL {
-                formatter.maximumFractionDigits = 1
-            }
-            formatter.roundingMode = .halfUp
-            return formatter
-        }
-
-        private var carbsFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            return formatter
-        }
-
-        var body: some View {
-            Form {
-                Section {
-                    Toggle("Use Dynamic BG Color", isOn: $state.dynamicGlucoseColor)
-                    Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
-                    Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
-                    Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)
-                    Toggle("Standing / Laying TIR Chart", isOn: $state.oneDimensionalGraph)
-                    Toggle("Enable total insulin in scope", isOn: $state.tins)
-                    HStack {
-                        Text("Hours X-Axis (6 default)")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.hours, placeholder: "6", numberFormatter: carbsFormatter)
-                        Text("hours").foregroundColor(.secondary)
-                    }
-                    Toggle("Show Forecasts as Lines", isOn: $state.displayForecastsAsLines)
-                } header: { Text("Home Chart settings ") }
-
-                Section {
-                    HStack {
-                        Text("Low")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("High")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.high, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    Toggle("Override HbA1c Unit", isOn: $state.overrideHbA1cUnit)
-
-                } header: { Text("Statistics settings ") }
-
-                Section {
-                    Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)
-                    Toggle("Display and allow Fat and Protein entries", isOn: $state.useFPUconversion)
-                } header: { Text("Add Meal View settings ") }
-
-                Section {
-                    Picker(
-                        selection: $state.historyLayout,
-                        label: Text("History Layout")
-                    ) {
-                        ForEach(HistoryLayout.allCases) { selection in
-                            Text(selection.displayName).tag(selection)
-                        }
-                    }
-                } header: { Text("History Settings") }
-
-                Section {
-                    Picker(
-                        selection: $state.lockScreenView,
-                        label: Text("Lock screen widget")
-                    ) {
-                        ForEach(LockScreenView.allCases) { selection in
-                            Text(selection.displayName).tag(selection)
-                        }
-                    }
-                } header: { Text("Lock screen widget") }
-            }
-            .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
-            .navigationBarTitle("UI/UX")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
-    }
-}

+ 6 - 0
Trio/Sources/Modules/Treatments/TreatmentsProvider.swift

@@ -33,5 +33,11 @@ extension Treatments {
                     sensitivities: []
                 )
         }
+
+        func getPreferences() async -> Preferences {
+            await storage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self)
+                ?? Preferences(from: OpenAPS.defaults(for: OpenAPS.Settings.preferences))
+                ?? Preferences(maxIOB: 0, maxCOB: 120)
+        }
     }
 }

+ 40 - 5
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -31,6 +31,8 @@ extension Treatments {
         var threshold: Decimal = 0
         var maxBolus: Decimal = 0
         var maxExternal: Decimal { maxBolus * 3 }
+        var maxIOB: Decimal = 0
+        var maxCOB: Decimal = 0
         var errorString: Decimal = 0
         var evBG: Decimal = 0
         var insulin: Decimal = 0
@@ -40,6 +42,7 @@ extension Treatments {
         var minDelta: Decimal = 0
         var expectedDelta: Decimal = 0
         var minPredBG: Decimal = 0
+        var lastLoopDate: Date?
         var isAwaitingDeterminationResult: Bool = false
         var carbRatio: Decimal = 0
 
@@ -58,6 +61,7 @@ extension Treatments {
         var wholeCobInsulin: Decimal = 0
         var iobInsulinReduction: Decimal = 0
         var wholeCalc: Decimal = 0
+        var factoredInsulin: Decimal = 0
         var insulinCalculated: Decimal = 0
         var fraction: Decimal = 0
         var basal: Decimal = 0
@@ -65,6 +69,7 @@ extension Treatments {
         var fattyMealFactor: Decimal = 0
         var useFattyMealCorrectionFactor: Bool = false
         var displayPresets: Bool = true
+        var confirmBolus: Bool = false
 
         var currentBasal: Decimal = 0
         var currentCarbRatio: Decimal = 0
@@ -157,8 +162,10 @@ extension Treatments {
 
         deinit {
             // Unregister from broadcaster
-            broadcaster.unregister(DeterminationObserver.self, observer: self)
-            broadcaster.unregister(BolusFailureObserver.self, observer: self)
+            if let broadcaster = broadcaster {
+                broadcaster.unregister(DeterminationObserver.self, observer: self)
+                broadcaster.unregister(BolusFailureObserver.self, observer: self)
+            }
 
             // Cancel Combine subscriptions
             unsubscribe()
@@ -242,6 +249,13 @@ extension Treatments {
                         self.maxBolus = getMaxBolus
                     }
                 }
+                group.addTask {
+                    let getPreferences = await self.provider.getPreferences()
+                    await MainActor.run {
+                        self.maxIOB = getPreferences.maxIOB
+                        self.maxCOB = getPreferences.maxCOB
+                    }
+                }
             }
         }
 
@@ -272,6 +286,7 @@ extension Treatments {
             sweetMeals = settings.settings.sweetMeals
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
+            confirmBolus = settings.settings.confirmBolus
             forecastDisplayType = settings.settings.forecastDisplayType
             lowGlucose = settingsManager.settings.low
             highGlucose = settingsManager.settings.high
@@ -371,6 +386,7 @@ extension Treatments {
                 iobInsulinReduction = result.iobInsulinReduction
                 superBolusInsulin = result.superBolusInsulin
                 wholeCalc = result.wholeCalc
+                factoredInsulin = result.factoredInsulin
                 fifteenMinInsulin = result.fifteenMinutesInsulin
             }
 
@@ -671,11 +687,28 @@ extension Treatments.StateModel {
     }
 
     @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+        // Store all objects for the forecast graph
         glucoseFromPersistence = objects
 
-        let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
-        let thirdLastGlucose = glucoseFromPersistence.dropFirst(2).first?.glucose ?? 0
-        let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+        // Always use the most recent reading for current glucose
+        let lastGlucose = objects.first?.glucose ?? 0
+
+        // Filter for readings less than 20 minutes old
+        let twentyMinutesAgo = Date().addingTimeInterval(-20 * 60)
+        let recentObjects = objects.filter {
+            guard let date = $0.date else { return false }
+            return date > twentyMinutesAgo
+        }
+
+        // Calculate delta using newest and oldest readings within 20-minute window
+        let delta: Decimal
+        if let newestInWindow = recentObjects.first?.glucose, let oldestInWindow = recentObjects.last?.glucose {
+            // Newest is at index 0, oldest is at the last index
+            delta = Decimal(newestInWindow) - Decimal(oldestInWindow)
+        } else {
+            // Not enough data points in the window
+            delta = 0
+        }
 
         currentBG = Decimal(lastGlucose)
         deltaBG = delta
@@ -763,6 +796,8 @@ extension Treatments.StateModel {
             // setup vars for bolus calculation
             insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
             evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
+            minPredBG = (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal
+            lastLoopDate = apsManager.lastLoopDate as Date?
             insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
             target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
             isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal

+ 16 - 10
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -83,16 +83,9 @@ struct ForecastChart: View {
                 Image(systemName: "arrow.right.circle")
 
                 if let simulatedDetermination = state.simulatedDetermination, let eventualBG = simulatedDetermination.eventualBG {
-                    HStack {
-                        Text(
-                            state.units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
-                        )
-                        .font(.footnote)
-                        .foregroundStyle(.primary)
-                        Text("\(state.units.rawValue)")
-                            .font(.footnote)
-                            .foregroundStyle(.secondary)
-                    }
+                    eventualGlucoseBadge(for: eventualBG)
+                } else if let lastDetermination = state.determination.first, let eventualBG = lastDetermination.eventualBG {
+                    eventualGlucoseBadge(for: Int(truncating: eventualBG))
                 } else {
                     Text("---")
                         .font(.footnote)
@@ -112,6 +105,19 @@ struct ForecastChart: View {
         }
     }
 
+    @ViewBuilder private func eventualGlucoseBadge(for eventualBG: Int) -> some View {
+        HStack {
+            Text(
+                state.units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
+            )
+            .font(.footnote)
+            .foregroundStyle(.primary)
+            Text("\(state.units.rawValue)")
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+        }
+    }
+
     private var forecastChart: some View {
         Chart {
             drawGlucose()

File diff ditekan karena terlalu besar
+ 870 - 379
Trio/Sources/Modules/Treatments/View/PopupView.swift


+ 67 - 19
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -391,8 +391,6 @@ extension Treatments {
             }
             .sheet(isPresented: $state.showInfo) {
                 PopupView(state: state)
-                    .presentationDetents([.fraction(0.9), .large])
-                    .presentationDragIndicator(.visible)
             }
             .sheet(isPresented: $showPresetSheet, onDismiss: {
                 showPresetSheet = false
@@ -421,6 +419,28 @@ extension Treatments {
             }
         }
 
+        @State private var showConfirmDialogForBolusing = false
+
+        private var bolusWarning: (shouldConfirm: Bool, warningMessage: String, color: Color) {
+            let isGlucoseVeryLow = state.currentBG < 54
+            let isForecastVeryLow = state.minPredBG < 54
+
+            // Only warn when enacting a bolus via pump
+            guard !state.externalInsulin, state.amount > 0 else {
+                return (false, "", .primary)
+            }
+
+            let warningMessage = isGlucoseVeryLow ? String(localized: "Glucose is very low.") :
+                isForecastVeryLow ? String(localized: "Glucose forecast is very low.") :
+                ""
+
+            let warningColor: Color = isGlucoseVeryLow ? .red : colorScheme == .dark ? .orange : .accentColor
+
+            let shouldConfirm = state.confirmBolus && (isGlucoseVeryLow || isForecastVeryLow)
+
+            return (shouldConfirm, warningMessage, warningColor)
+        }
+
         var treatmentButton: some View {
             var treatmentButtonBackground = Color(.systemBlue)
             if limitExceeded {
@@ -429,26 +449,54 @@ extension Treatments {
                 treatmentButtonBackground = Color(.systemGray)
             }
 
-            return Button {
-                state.invokeTreatmentsTask()
-            } label: {
-                HStack {
-                    if state.isBolusInProgress && state
-                        .amount > 0 && !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
-                    {
-                        ProgressView()
+            return Section {
+                Button {
+                    if bolusWarning.shouldConfirm {
+                        showConfirmDialogForBolusing = true
+                    } else {
+                        state.invokeTreatmentsTask()
+                    }
+                } label: {
+                    HStack {
+                        if state.isBolusInProgress && state.amount > 0 &&
+                            !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
+                        {
+                            ProgressView()
+                        }
+                        taskButtonLabel
                     }
-                    taskButtonLabel
+                    .font(.headline)
+                    .foregroundStyle(Color.white)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .frame(height: 35)
+                }
+                .disabled(disableTaskButton)
+                .listRowBackground(treatmentButtonBackground)
+                .shadow(radius: 3)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .confirmationDialog(
+                    bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
+                    isPresented: $showConfirmDialogForBolusing,
+                    titleVisibility: .visible
+                ) {
+                    Button("Cancel", role: .cancel) {}
+                    Button(
+                        bolusWarning.warningMessage.isEmpty ? "Enact Bolus" : "Ignore Warning and Enact Bolus",
+                        role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                    ) {
+                        state.invokeTreatmentsTask()
+                    }
+                }
+            } header: {
+                if !bolusWarning.warningMessage.isEmpty {
+                    Text(bolusWarning.warningMessage)
+                        .textCase(nil)
+                        .font(.subheadline)
+                        .foregroundColor(bolusWarning.color)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .padding(.top, -22)
                 }
-                .font(.headline)
-                .foregroundStyle(Color.white)
-                .frame(maxWidth: .infinity, alignment: .center)
-                .frame(height: 35)
             }
-            .disabled(disableTaskButton)
-            .listRowBackground(treatmentButtonBackground)
-            .shadow(radius: 3)
-            .clipShape(RoundedRectangle(cornerRadius: 8))
         }
 
         private var taskButtonLabel: some View {

+ 2 - 2
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -12,7 +12,7 @@ extension UserInterfaceSettings {
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
-        @Published var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
+        @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
 
         var units: GlucoseUnits = .mgdL
 
@@ -41,7 +41,7 @@ extension UserInterfaceSettings {
 
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
 
-            subscribeSetting(\.timeInRangeChartStyle, on: $timeInRangeChartStyle) { timeInRangeChartStyle = $0 }
+            subscribeSetting(\.timeInRangeType, on: $timeInRangeType) { timeInRangeType = $0 }
         }
     }
 }

+ 52 - 13
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -286,7 +286,7 @@ extension UserInterfaceSettings {
 
                             HStack(alignment: .center) {
                                 Text(
-                                    "Set low and high glucose values for the main screen glucose graph and statistics."
+                                    "Set low and high glucose values for the main screen, watch app and live activity glucose graph."
                                 )
                                 .lineLimit(nil)
                                 .font(.footnote)
@@ -382,7 +382,7 @@ extension UserInterfaceSettings {
                         VStack {
                             Picker(
                                 selection: $state.eA1cDisplayUnit,
-                                label: Text("eA1c Display Unit")
+                                label: Text("eA1c/GMI Display Unit")
                             ) {
                                 ForEach(EstimatedA1cDisplayUnit.allCases) { selection in
                                     Text(selection.displayName).tag(selection)
@@ -391,7 +391,7 @@ extension UserInterfaceSettings {
 
                             HStack(alignment: .center) {
                                 Text(
-                                    "Choose to display eA1c in percent or mmol/mol."
+                                    "Choose to display eA1c and GMI in percent or mmol/mol."
                                 )
                                 .font(.footnote)
                                 .foregroundColor(.secondary)
@@ -399,11 +399,11 @@ extension UserInterfaceSettings {
                                 Spacer()
                                 Button(
                                     action: {
-                                        hintLabel = String(localized: "eA1c Display Unit")
+                                        hintLabel = String(localized: "eA1c/GMI Display Unit")
                                         selectedVerboseHint =
                                             AnyView(
                                                 Text(
-                                                    "Choose which format you'd prefer the eA1c (estimated A1c) value in the statistics view as a percentage (Example: 6.5%) or mmol/mol (Example: 48 mmol/mol)."
+                                                    "Choose which format you'd prefer the eA1c (estimated A1c) and GMI (Glucose Management Index) value in the statistics view as a percentage (Example: eA1c: 6.5%) or mmol/mol (Example: eA1c: 48 mmol/mol)."
                                                 )
                                             )
                                         shouldDisplayHint.toggle()
@@ -422,17 +422,17 @@ extension UserInterfaceSettings {
                 Section {
                     VStack(alignment: .leading) {
                         Picker(
-                            selection: $state.timeInRangeChartStyle,
-                            label: Text("Time in Range Chart Style").multilineTextAlignment(.leading)
+                            selection: $state.timeInRangeType,
+                            label: Text("Time in Range Type").multilineTextAlignment(.leading)
                         ) {
-                            ForEach(TimeInRangeChartStyle.allCases) { selection in
+                            ForEach(TimeInRangeType.allCases) { selection in
                                 Text(selection.displayName).tag(selection)
                             }
                         }.padding(.top)
 
                         HStack(alignment: .center) {
                             Text(
-                                "Choose the orientation of the Time in Range Chart."
+                                "Choose type of time in range to be used for Trio's statistics."
                             )
                             .font(.footnote)
                             .foregroundColor(.secondary)
@@ -440,12 +440,51 @@ extension UserInterfaceSettings {
                             Spacer()
                             Button(
                                 action: {
-                                    hintLabel = String(localized: "Time in Range Chart Style")
+                                    hintLabel = String(localized: "Time in Range Type")
                                     selectedVerboseHint =
                                         AnyView(
-                                            Text(
-                                                "Choose which style for the time in range chart you'd prefer: a standing, i.e., vertical, bar chart or a laying, i.e., horizontal, line chart."
-                                            )
+                                            VStack(
+                                                alignment: .leading,
+                                                spacing: 10
+                                            ) {
+                                                Text(
+                                                    "Choose which type of time in range Trio should adopt for all its statistical charts and displays:"
+                                                )
+                                                VStack(
+                                                    alignment: .leading,
+                                                    spacing: 5
+                                                ) {
+                                                    Text(
+                                                        "Time in Tight Range (TITR):"
+                                                    )
+                                                    .bold()
+                                                    let titrBottomThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(70) : 70.asMmolL)"
+                                                    let titrTopThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(140) : 140.asMmolL)"
+                                                    Text(String(
+                                                        localized: "Uses the fairly established Time in Tight Range definition, which is defined as time between \(titrBottomThreshold) and \(titrTopThreshold)  \(state.units.rawValue).",
+                                                        comment: "Time in Tight Range (TITR) verbose hint description"
+                                                    ))
+                                                }
+                                                VStack(
+                                                    alignment: .leading,
+                                                    spacing: 5
+                                                ) {
+                                                    Text(
+                                                        "Time in Normoglycemia (TING):"
+                                                    )
+                                                    .bold()
+                                                    let tingBottomThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(63) : 63.asMmolL)"
+                                                    let tingTopThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(140) : 140.asMmolL)"
+                                                    Text(String(
+                                                        localized: "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (\(tingBottomThreshold) \(state.units.rawValue)) and \(tingTopThreshold) \(state.units.rawValue).",
+                                                        comment: "Time in Normoglycemia (TING) verbose hint description"
+                                                    ))
+                                                }
+                                            }
                                         )
                                     shouldDisplayHint.toggle()
                                 },

+ 87 - 22
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -30,6 +30,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     private struct BolusCalculatorVariables {
         var insulinRequired: Decimal
         var evBG: Decimal
+        var minPredBG: Decimal
+        var lastLoopDate: Date?
         var insulin: Decimal
         var target: Decimal
         var isf: Decimal
@@ -170,6 +172,14 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             )
     }
 
+    /// Retrieves Preferences from storage
+    /// - Returns: Preferences object containing maxIOB and maxCOB
+    private func getPreferences() async -> Preferences {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.preferences, as: Preferences.self)
+            ?? Preferences(from: OpenAPS.defaults(for: OpenAPS.Settings.preferences))
+            ?? Preferences(maxIOB: 0, maxCOB: 120)
+    }
+
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
@@ -194,9 +204,27 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     /// - Parameter objects: Array of GlucoseStored objects
     /// - Returns: GlucoseVariables containing current blood glucose and delta
     private func updateGlucoseVariables(with objects: [GlucoseStored]) -> GlucoseVariables {
+        // Always use the most recent reading for current glucose regardless of time
         let lastGlucose = objects.first?.glucose ?? 0
-        let thirdLastGlucose = objects.dropFirst(2).first?.glucose ?? 0
-        let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+
+        // Filter for readings less than 20 minutes old
+        let twentyMinutesAgo = Date().addingTimeInterval(-20 * 60)
+        let recentObjects = objects.filter {
+            guard let date = $0.date else { return false }
+            return date > twentyMinutesAgo
+        }
+
+        // Calculate delta using newest and oldest readings within 20-minute window
+        let delta: Decimal
+        if recentObjects.count >= 2 {
+            // Newest is at index 0, oldest is at the last index
+            let newestInWindow = recentObjects.first?.glucose ?? 0
+            let oldestInWindow = recentObjects.last?.glucose ?? 0
+            delta = Decimal(newestInWindow) - Decimal(oldestInWindow)
+        } else {
+            // Not enough data points in the window
+            delta = 0
+        }
 
         return GlucoseVariables(currentBG: Decimal(lastGlucose), deltaBG: delta)
     }
@@ -220,6 +248,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             return BolusCalculatorVariables(
                 insulinRequired: 0,
                 evBG: 0,
+                minPredBG: 0,
+                lastLoopDate: nil,
                 insulin: 0,
                 target: currentBGTarget,
                 isf: currentISF,
@@ -234,6 +264,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         return BolusCalculatorVariables(
             insulinRequired: (mostRecentDetermination.insulinReq ?? 0) as Decimal,
             evBG: (mostRecentDetermination.eventualBG ?? 0) as Decimal,
+            minPredBG: (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal,
+            lastLoopDate: apsManager.lastLoopDate as Date?,
             insulin: (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal,
             target: (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal,
             isf: (mostRecentDetermination.insulinSensitivity ?? NSDecimalNumber(decimal: currentISF)) as Decimal,
@@ -263,6 +295,12 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
             let currentISF = await getCurrentSettingValue(for: .isf)
 
+            // Get max IOB and max COB
+
+            let preferences = await getPreferences()
+            let maxIOB = preferences.maxIOB
+            let maxCOB = preferences.maxCOB
+
             // Fetch glucose data
             let glucoseIds = try await fetchGlucose()
             let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared.getNSManagedObject(
@@ -306,7 +344,10 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 sweetMealFactor: settings.sweetMealFactor,
                 basal: bolusVars.basal,
                 fraction: settings.fraction,
-                maxBolus: maxBolus
+                maxBolus: maxBolus,
+                maxIOB: maxIOB,
+                maxCOB: maxCOB,
+                minPredBG: bolusVars.minPredBG
             )
         } catch {
             debug(
@@ -324,14 +365,15 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     func calculateInsulin(input: CalculationInput) async -> CalculationResult {
         // insulin needed for the current blood glucose
         let targetDifference = input.currentBG - input.target
-        let targetDifferenceInsulin = apsManager.roundBolus(amount: targetDifference / input.isf)
+
+        let targetDifferenceInsulin = targetDifference / input.isf
 
         // more or less insulin because of bg trend in the last 15 minutes
-        let fifteenMinutesInsulin = apsManager.roundBolus(amount: input.deltaBG / input.isf)
+        let fifteenMinutesInsulin = input.deltaBG / input.isf
 
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
-        let wholeCob = Decimal(input.cob) + input.carbs
-        let wholeCobInsulin = apsManager.roundBolus(amount: wholeCob / input.carbRatio)
+        let wholeCob = min(Decimal(input.cob) + input.carbs, input.maxCOB)
+        let wholeCobInsulin = wholeCob / input.carbRatio
 
         // determine how much the calculator reduces/ increases the bolus because of IOB
         let iobInsulinReduction = (-1) * input.iob
@@ -352,29 +394,47 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         }
 
         // apply custom factor at the end of the calculations
-        let result = wholeCalc * input.fraction
-
         // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
-        var insulinCalculated: Decimal
+        var factoredInsulin = wholeCalc
+
+        // Apply Recommended Bolus Percentage (input.fraction) and if selected apply Fatty Meal Bolus Percentage (input.fattyMealFactor)
+        // If factoredInsulin is negative, though, don't apply either
+        if factoredInsulin > 0 {
+            factoredInsulin *= input.fraction
+
+            if input.useFattyMealCorrectionFactor {
+                factoredInsulin *= input.fattyMealFactor
+            }
+        }
+
+        // Calculate and add super bolus insulin if enabled
         var superBolusInsulin: Decimal = 0
-        if input.useFattyMealCorrectionFactor {
-            insulinCalculated = result * input.fattyMealFactor
-        } else if input.useSuperBolus {
+        if input.useSuperBolus {
             superBolusInsulin = input.sweetMealFactor * input.basal
-            insulinCalculated = result + superBolusInsulin
-        } else {
-            insulinCalculated = result
+            factoredInsulin += superBolusInsulin
         }
 
-        // display no negative insulinCalculated
-        insulinCalculated = max(insulinCalculated, 0)
-        insulinCalculated = min(insulinCalculated, input.maxBolus)
+        // the final result for recommended insulin amount
+        var insulinCalculated: Decimal
+        let isLoopStale = Date().timeIntervalSince(apsManager.lastLoopDate) > 15 * 60
 
-        // round calculated recommendation to allowed bolus increment
-        insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+        // don't recommend insulin when current glucose or minPredBG is < 54 or last sucessful loop was over 15 minutes ago
+        if input.currentBG < 54 || input.minPredBG < 54 || isLoopStale {
+            insulinCalculated = 0
+        } else {
+            // no negative insulinCalculated
+            insulinCalculated = max(factoredInsulin, 0)
+            // don't exceed maxBolus
+            insulinCalculated = min(insulinCalculated, input.maxBolus)
+            // don't exceed maxIOB
+            insulinCalculated = min(insulinCalculated, input.maxIOB - input.iob)
+            // round calculated recommendation to allowed bolus increment
+            insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+        }
 
         return CalculationResult(
             insulinCalculated: insulinCalculated,
+            factoredInsulin: factoredInsulin,
             wholeCalc: wholeCalc,
             correctionInsulin: targetDifferenceInsulin,
             iobInsulinReduction: iobInsulinReduction,
@@ -414,6 +474,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             // Return safe default values
             return CalculationResult(
                 insulinCalculated: 0,
+                factoredInsulin: 0,
                 wholeCalc: 0,
                 correctionInsulin: 0,
                 iobInsulinReduction: 0,
@@ -445,11 +506,15 @@ struct CalculationInput: Sendable {
     let basal: Decimal // Current basal rate
     let fraction: Decimal // General correction factor
     let maxBolus: Decimal // Maximum allowed bolus
+    let maxIOB: Decimal // Maximum allowed IOB to be used for rec. bolus calculation
+    let maxCOB: Decimal // Maximum allowed COB to be used for rec. bolus calculation
+    let minPredBG: Decimal // Minimum Predicted Glucose determined by Oref
 }
 
 /// Results of the bolus calculation
 struct CalculationResult: Sendable {
-    let insulinCalculated: Decimal // Final calculated insulin amount
+    let insulinCalculated: Decimal // Final calculated insulin amount which respects limits
+    let factoredInsulin: Decimal // Total calculation after adjustments
     let wholeCalc: Decimal // Total calculation before adjustments
     let correctionInsulin: Decimal // Insulin for BG correction
     let iobInsulinReduction: Decimal // IOB reduction amount

+ 70 - 10
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -504,6 +504,19 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             return
         }
 
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TDDStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        let tdd: Decimal? = await backgroundContext.perform {
+            (results as? [TDDStored])?.first?.total as? Decimal
+        }
+
         // Suggested / Enacted
         async let enactedDeterminationID = determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDeterminationsNotYetUploadedToNightscout)
@@ -543,6 +556,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 suggestion.minPredBG = suggestion.minPredBG?.asMmolL
                 suggestion.threshold = suggestion.threshold?.asMmolL
             }
+
+            suggestion.reason = injectTDD(into: suggestion.reason, tdd: tdd)
+            suggestion.tdd = tdd
+
             // Check whether the last suggestion that was uploaded is the same that is fetched again when we are attempting to upload the enacted determination
             // Apparently we are too fast; so the flag update is not fast enough to have the predicate filter last suggestion out
             // If this check is truthy, set suggestion to nil so it's not uploaded again
@@ -553,17 +570,20 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
         }
 
-        if let fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
-            var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
-            modifiedFetchedEnactedDetermination?
-                .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)
-            // TODO: verify that these parsings are needed for 3rd party apps, e.g., LoopFollow
-            modifiedFetchedEnactedDetermination?.current_target = fetchedEnacted.current_target?.asMmolL
-            modifiedFetchedEnactedDetermination?.minGuardBG = fetchedEnacted.minGuardBG?.asMmolL
-            modifiedFetchedEnactedDetermination?.minPredBG = fetchedEnacted.minPredBG?.asMmolL
-            modifiedFetchedEnactedDetermination?.threshold = fetchedEnacted.threshold?.asMmolL
+        if var enacted = fetchedEnactedDetermination {
+            if settingsManager.settings.units == .mmolL {
+                enacted.reason = parseReasonGlucoseValuesToMmolL(enacted.reason)
+                // TODO: verify that these parsings are needed for 3rd party apps, e.g., LoopFollow
+                enacted.current_target = enacted.current_target?.asMmolL
+                enacted.minGuardBG = enacted.minGuardBG?.asMmolL
+                enacted.minPredBG = enacted.minPredBG?.asMmolL
+                enacted.threshold = enacted.threshold?.asMmolL
+            }
 
-            fetchedEnactedDetermination = modifiedFetchedEnactedDetermination
+            enacted.reason = injectTDD(into: enacted.reason, tdd: tdd)
+            enacted.tdd = tdd
+
+            fetchedEnactedDetermination = enacted
         }
 
         // Gather all relevant data for OpenAPS Status
@@ -1453,3 +1473,43 @@ extension BaseNightscoutManager {
         return updatedReason
     }
 }
+
+extension BaseNightscoutManager {
+    /// Injects TDD into the provided `reason` string if TDD is available.
+    ///
+    /// - Parameters:
+    ///   - reason: The raw reason string (e.g., "minPredBG=5.2, IOBpredBG=102").
+    ///   - tdd: The total daily dose of insulin.
+    /// - Returns: A modified reason string that includes "TDD: x U" appended
+    ///   after the last matched prediction term, or at the end if no match is found.
+    func injectTDD(into reason: String, tdd: Decimal?) -> String {
+        guard let tdd = tdd else { return reason }
+
+        let tddString = ", TDD: \(tdd) U"
+
+        // Regex that matches any of the keywords followed by an optional colon, whitespace, then a number.
+        let pattern = "(minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG):?\\s*(-?\\d+(?:\\.\\d+)?)"
+        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
+            return reason + tddString
+        }
+
+        // Split the reason at the first semicolon (if present)
+        let components = reason.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false)
+        let mainPart = String(components[0])
+        let tailPart = components.count > 1 ? ";" + components[1] : ""
+
+        // Search only in the main part for the keywords
+        let nsRange = NSRange(mainPart.startIndex ..< mainPart.endIndex, in: mainPart)
+        let matches = regex.matches(in: mainPart, options: [], range: nsRange)
+
+        // If found, insert TDD after the last occurrence in the main part.
+        if let lastMatch = matches.last, let matchRange = Range(lastMatch.range, in: mainPart) {
+            var modifiedMainPart = mainPart
+            modifiedMainPart.insert(contentsOf: tddString, at: matchRange.upperBound)
+            return modifiedMainPart + tailPart
+        }
+
+        // If no match is found, append TDD at the end of the original reason string.
+        return reason + tddString
+    }
+}

+ 4 - 3
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -5,6 +5,7 @@ import HealthKit
 import LoopKit
 import LoopKitUI
 import Swinject
+import TidepoolServiceKit
 
 protocol TidepoolManager {
     func addTidepoolService(service: Service)
@@ -96,12 +97,12 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
     /// Loads the Tidepool service from raw stored data
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
-        guard let rawState = rawValue["state"] as? Service.RawStateValue,
-              let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
+        let serviceType = TidepoolService.self
+        guard let rawState = rawValue["state"] as? Service.RawStateValue
         else { return nil }
 
         if let service = serviceType.init(rawState: rawState) {
-            return service as? RemoteDataService
+            return service as RemoteDataService
         }
         return nil
     }

+ 2 - 2
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -45,8 +45,8 @@ extension TrioRemoteControl {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: taskContext,
-            predicate: NSPredicate(format: "createdAt > %@", pushMessageDate as NSDate),
-            key: "createdAt",
+            predicate: NSPredicate(format: "date > %@", pushMessageDate as NSDate),
+            key: "date",
             ascending: false
         )
 

+ 12 - 3
Trio/Sources/Views/SettingInputSection.swift

@@ -33,6 +33,8 @@ struct SettingInputSection<VerboseHint: View>: View {
     var verboseHint: VerboseHint
     var headerText: String?
     var footerText: String?
+    var isToggleDisabled: Bool = false
+    var miniHintColor: Color = .secondary
 
     @ObservedObject private var pickerSettingsProvider = PickerSettingsProvider.shared
     @State private var displayPicker: Bool = false
@@ -55,6 +57,7 @@ struct SettingInputSection<VerboseHint: View>: View {
 
                     case .boolean:
                         toggleView(label: label, isOn: $booleanValue)
+                            .disabled(isToggleDisabled)
 
                     case let .conditionalDecimal(key):
                         VStack {
@@ -73,7 +76,8 @@ struct SettingInputSection<VerboseHint: View>: View {
                     hintSection(
                         miniHint: miniHint,
                         shouldDisplayHint: $shouldDisplayHint,
-                        verboseHint: verboseHint
+                        verboseHint: verboseHint,
+                        miniHintColor: miniHintColor
                     )
                 }
             },
@@ -235,11 +239,16 @@ struct SettingInputSection<VerboseHint: View>: View {
         }.padding(.top)
     }
 
-    private func hintSection(miniHint: String, shouldDisplayHint: Binding<Bool>, verboseHint: VerboseHint) -> some View {
+    private func hintSection(
+        miniHint: String,
+        shouldDisplayHint: Binding<Bool>,
+        verboseHint: VerboseHint,
+        miniHintColor: Color = .secondary
+    ) -> some View {
         HStack(alignment: .center) {
             Text(miniHint)
                 .font(.footnote)
-                .foregroundColor(.secondary)
+                .foregroundColor(miniHintColor)
                 .lineLimit(nil)
             Spacer()
             Button(action: {

+ 29 - 5
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

@@ -37,6 +37,9 @@ import Testing
         let basal: Decimal = 1.5
         let fraction: Decimal = 0.8
         let maxBolus: Decimal = 10
+        let maxIOB: Decimal = 15.0
+        let maxCOB: Decimal = 120.0
+        let minPredBG: Decimal = 80.0
 
         // STEP 2: Create calculation input
         let input = CalculationInput(
@@ -54,7 +57,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
 
         // STEP 3: Calculate insulin
@@ -144,6 +150,9 @@ import Testing
         let basal: Decimal = 1.5
         let fraction: Decimal = 0.8
         let maxBolus: Decimal = 10
+        let maxIOB: Decimal = 15.0
+        let maxCOB: Decimal = 120.0
+        let minPredBG: Decimal = 80.0
 
         // STEP 2: Create calculation input
         let input = CalculationInput(
@@ -161,7 +170,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
 
         // STEP 3: Calculate insulin with fatty meal enabled
@@ -183,7 +195,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         let standardResult = await calculator.calculateInsulin(input: standardInput)
 
@@ -224,6 +239,9 @@ import Testing
         let basal: Decimal = 1.5 // Will be added to insulin calculation when super bolus is enabled
         let fraction: Decimal = 0.8
         let maxBolus: Decimal = 10
+        let maxIOB: Decimal = 15.0
+        let maxCOB: Decimal = 120.0
+        let minPredBG: Decimal = 80.0
 
         // STEP 2: Create calculation input with super bolus enabled
         let input = CalculationInput(
@@ -241,7 +259,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
 
         // STEP 3: Calculate insulin with super bolus enabled
@@ -263,7 +284,10 @@ import Testing
             sweetMealFactor: sweetMealFactor,
             basal: basal,
             fraction: fraction,
-            maxBolus: maxBolus
+            maxBolus: maxBolus,
+            maxIOB: maxIOB,
+            maxCOB: maxCOB,
+            minPredBG: minPredBG
         )
         let standardResult = await calculator.calculateInsulin(input: standardInput)
 

+ 0 - 53
TrioTests/PluginManagerTests.swift

@@ -24,68 +24,15 @@ import Testing
             let cgmLoopManager = pluginManager.getCGMManagerTypeByIdentifier(cgmLoop.identifier)
             #expect(cgmLoopManager != nil, "Should load valid CGM manager")
         }
-
-        // When trying to load CGM manager with pump identifier
-        if let cgmLoop = cgmLoopManagers.last {
-            let invalidManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
-            #expect(invalidManager == nil, "Should not load CGM manager with pump identifier")
-        }
-    }
-
-    @Test("Can load pump managers") func testPumpManagerLoad() {
-        // Given
-        let pumpLoopManagers = pluginManager.availablePumpManagers
-
-        // Then
-        #expect(!pumpLoopManagers.isEmpty, "Should have available pump managers")
-
-        // When loading valid pump manager
-        if let pumpLoop = pumpLoopManagers.first {
-            let pumpLoopManager = pluginManager.getPumpManagerTypeByIdentifier(pumpLoop.identifier)
-            #expect(pumpLoopManager != nil, "Should load valid pump manager")
-        }
-
-        // When trying to load pump manager with CGM identifier
-        if let pumpLoop = pumpLoopManagers.last {
-            let invalidManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
-            #expect(invalidManager == nil, "Should not load pump manager with CGM identifier")
-        }
-    }
-
-    @Test("Can load service managers") func testServiceManagerLoad() {
-        // Given
-        let serviceManagers = pluginManager.availableServices
-
-        // Then
-        #expect(!serviceManagers.isEmpty, "Should have available services")
-
-        // When
-        if let serviceLoop = serviceManagers.first {
-            let serviceManager = pluginManager.getServiceTypeByIdentifier(serviceLoop.identifier)
-            #expect(serviceManager != nil, "Should load valid service manager")
-        }
     }
 
     @Test("Available managers have valid descriptors") func testManagerDescriptors() {
         // Given/When
-        let pumpManagers = pluginManager.availablePumpManagers
         let cgmManagers = pluginManager.availableCGMManagers
-        let serviceManagers = pluginManager.availableServices
-
-        // Then
-        for manager in pumpManagers {
-            #expect(!manager.identifier.isEmpty, "Pump manager should have non-empty identifier")
-            #expect(!manager.localizedTitle.isEmpty, "Pump manager should have non-empty title")
-        }
 
         for manager in cgmManagers {
             #expect(!manager.identifier.isEmpty, "CGM manager should have non-empty identifier")
             #expect(!manager.localizedTitle.isEmpty, "CGM manager should have non-empty title")
         }
-
-        for manager in serviceManagers {
-            #expect(!manager.identifier.isEmpty, "Service should have non-empty identifier")
-            #expect(!manager.localizedTitle.isEmpty, "Service should have non-empty title")
-        }
     }
 }

+ 1 - 0
patches/save_patches_here.md

@@ -0,0 +1 @@
+Trio workspace patches can be saved in this directory (Trio/patches/)

+ 0 - 41
scripts/copy-plugins.sh

@@ -1,41 +0,0 @@
-#!/bin/sh -e
-
-#  copy-plugins.sh
-#  Loop
-#
-#  Copyright © 2019 LoopKit Authors. All rights reserved.
-
-
-shopt -s nullglob
-
-# Copy device plugins
-function copy_plugins {
-    echo "Looking for plugins in $1"
-    for f in "$1"/*.loopplugin; do
-      plugin=$(basename "$f")
-      echo Copying plugin: $plugin to frameworks directory in app
-      plugin_path="$(readlink -f "$f" || echo "$f")"
-      plugin_as_framework_path="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${plugin%.*}.framework"
-      rsync -va --exclude=Frameworks "$plugin_path/." "${plugin_as_framework_path}"
-      # Rename .plugin to .framework
-      if [ "$EXPANDED_CODE_SIGN_IDENTITY" != "-" ] && [ "$EXPANDED_CODE_SIGN_IDENTITY" != "" ]; then
-        export CODESIGN_ALLOCATE=${DT_TOOLCHAIN_DIR}/usr/bin/codesign_allocate
-        echo "Signing ${plugin} with ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
-        /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none --preserve-metadata=identifier,entitlements,flags "$plugin_as_framework_path"
-      else
-        echo "Skipping signing, no identity set"
-      fi
-      for framework_path in "${f}"/Frameworks/*.framework; do
-        framework=$(basename "$framework_path")
-        echo "Copying plugin's framework $framework_path to ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/."
-        cp -avf "$framework_path" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/."
-        plugin_path="$(readlink -f "$f" || echo "$f")"
-        if [ "$EXPANDED_CODE_SIGN_IDENTITY" != "-" ] && [ "$EXPANDED_CODE_SIGN_IDENTITY" != "" ]; then
-          echo "Signing $framework for $plugin with $EXPANDED_CODE_SIGN_IDENTITY_NAME"
-          /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --timestamp=none --preserve-metadata=identifier,entitlements,flags "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/${framework}"
-        fi
-      done
-    done
-}
-
-copy_plugins "$BUILT_PRODUCTS_DIR"