Kaynağa Gözat

Merge pull request #1057 from nightscout/sync-dev-eversense-06042026

Sync dev as of 06-04-2026
Deniz Cengiz 3 ay önce
ebeveyn
işleme
703406d266
91 değiştirilmiş dosya ile 11172 ekleme ve 2314 silme
  1. 3 0
      .github/FUNDING.yml
  2. 2 2
      .github/workflows/add_identifiers.yml
  3. 1 1
      .github/workflows/auto_version_dev.yml
  4. 6 6
      .github/workflows/build_trio.yml
  5. 4 4
      .github/workflows/create_certs.yml
  6. 2 2
      .github/workflows/stale_issues.yml
  7. 7 7
      .github/workflows/unit_tests.yml
  8. 2 2
      .github/workflows/validate_secrets.yml
  9. 4 0
      .gitmodules
  10. 1 1
      Config.xcconfig
  11. 1 1
      DanaKit
  12. 1 1
      LoopKit
  13. 1 0
      MedtrumKit
  14. 2 1
      Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift
  15. 5 0
      Model/Helper/PumpEvent+helper.swift
  16. 2 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  17. 66 0
      Trio Watch App Extension/Helper/WatchNotificationHandler.swift
  18. 5 0
      Trio Watch App Extension/TrioWatchApp.swift
  19. 94 35
      Trio.xcodeproj/project.pbxproj
  20. 3 0
      Trio.xcworkspace/contents.xcworkspacedata
  21. 2 2
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  22. 5 5
      Trio/Resources/Info.plist
  23. 3 111
      Trio/Resources/InfoPlist.xcstrings
  24. 8 3
      Trio/Resources/json/defaults/freeaps/freeaps_settings.json
  25. 12 2
      Trio/Sources/APS/APSManager.swift
  26. 50 0
      Trio/Sources/APS/DeviceDataManager.swift
  27. 224 47
      Trio/Sources/APS/FetchGlucoseManager.swift
  28. 108 12
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  29. 114 59
      Trio/Sources/APS/Storage/CarbsStorage.swift
  30. 6 3
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  31. 5 0
      Trio/Sources/Helpers/Decimal+Extensions.swift
  32. 7 0
      Trio/Sources/Helpers/Formatters.swift
  33. 0 172
      Trio/Sources/Helpers/SavitzkyGolayFilter.swift
  34. 7472 1217
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  35. 86 0
      Trio/Sources/Models/AlgorithmGlucose.swift
  36. 0 17
      Trio/Sources/Models/Autotune.swift
  37. 0 12
      Trio/Sources/Models/BloodGlucose.swift
  38. 22 0
      Trio/Sources/Models/BolusDisplayThreshold.swift
  39. 1 2
      Trio/Sources/Models/DecimalPickerSettings.swift
  40. 0 24
      Trio/Sources/Models/FetchedProfile.swift
  41. 124 0
      Trio/Sources/Models/GarminWatchSettings.swift
  42. 118 17
      Trio/Sources/Models/GarminWatchState.swift
  43. 64 0
      Trio/Sources/Models/NotificationIdentifiers.swift
  44. 86 8
      Trio/Sources/Models/TrioSettings.swift
  45. 3 0
      Trio/Sources/Models/WatchMessageKeys.swift
  46. 24 5
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  47. 21 3
      Trio/Sources/Modules/History/View/HistoryRootView.swift
  48. 9 0
      Trio/Sources/Modules/Home/HomeStateModel.swift
  49. 16 2
      Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift
  50. 27 37
      Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift
  51. 6 3
      Trio/Sources/Modules/Home/View/Chart/ChartElements/InsulinView.swift
  52. 14 3
      Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift
  53. 4 2
      Trio/Sources/Modules/Home/View/Chart/MainChartView.swift
  54. 45 25
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  55. 2 1
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  56. 2 1
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  57. 1 5
      Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  58. 4 34
      Trio/Sources/Modules/MealSettings/View/MealSettingsRootView.swift
  59. 6 2
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  60. 2 1
      Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift
  61. 2 1
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsSubstepView.swift
  62. 2 2
      Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift
  63. 3 0
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  64. 1 0
      Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift
  65. 5 0
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  66. 10 0
      Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift
  67. 4 4
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  68. 7 7
      Trio/Sources/Modules/Settings/SettingItems.swift
  69. 1 8
      Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift
  70. 31 2
      Trio/Sources/Modules/Snooze/SnoozeStateModel.swift
  71. 26 30
      Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift
  72. 1 1
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  73. 2 2
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift
  74. 2 2
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift
  75. 2 2
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift
  76. 39 17
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  77. 2 0
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  78. 42 0
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  79. 274 0
      Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminAppConfigView.swift
  80. 121 16
      Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminView.swift
  81. 19 1
      Trio/Sources/Modules/WatchConfig/WatchConfigStateModel.swift
  82. 112 13
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  83. 31 16
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  84. 204 0
      Trio/Sources/Services/WatchManager/FLOW_DIAGRAM.md
  85. 728 284
      Trio/Sources/Services/WatchManager/GarminManager.swift
  86. 0 2
      Trio/Sources/Views/SettingInputSection.swift
  87. 234 0
      TrioTests/CoreDataTests/CarbsStorageTests.swift
  88. 333 0
      TrioTests/GlucoseSmoothingTests.swift
  89. 2 2
      TrioTests/JSONImporterTests.swift
  90. 15 0
      TrioTests/Mocks/MockTDDStorage.swift
  91. 2 1
      scripts/swiftformat.sh

+ 3 - 0
.github/FUNDING.yml

@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+custom: ["https://www.nightscoutfoundation.org/donate"]

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

@@ -12,11 +12,11 @@ jobs:
   identifiers:
   identifiers:
     name: Add Identifiers
     name: Add Identifiers
     needs: validate
     needs: validate
-    runs-on: macos-15
+    runs-on: macos-26
     steps:
     steps:
       # Checks-out the repo
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
 
 
       # Patch Fastlane Match to not print tables
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
       - name: Patch Match Tables

+ 1 - 1
.github/workflows/auto_version_dev.yml

@@ -52,7 +52,7 @@ jobs:
 
 
     steps:
     steps:
       - name: Checkout repo
       - name: Checkout repo
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
         with:
          token: ${{ secrets.TRIO_TOKEN_AUTOBUMP }}
          token: ${{ secrets.TRIO_TOKEN_AUTOBUMP }}
 
 

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

@@ -90,7 +90,7 @@ jobs:
         if: |
         if: |
           steps.workflow-permission.outputs.has_permission == 'true' &&
           steps.workflow-permission.outputs.has_permission == 'true' &&
           (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
           (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
         with:
           token: ${{ secrets.GH_PAT }}
           token: ${{ secrets.GH_PAT }}
 
 
@@ -100,7 +100,7 @@ jobs:
           steps.workflow-permission.outputs.has_permission == 'true' &&
           steps.workflow-permission.outputs.has_permission == 'true' &&
           vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout'
           vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout'
         id: sync
         id: sync
-        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
+        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2
         with:
         with:
           target_sync_branch: ${{ env.TARGET_BRANCH }}
           target_sync_branch: ${{ env.TARGET_BRANCH }}
           shallow_since: 6 months ago
           shallow_since: 6 months ago
@@ -165,7 +165,7 @@ jobs:
   build:
   build:
     name: Build
     name: Build
     needs: [check_certs, check_status]
     needs: [check_certs, check_status]
-    runs-on: macos-15
+    runs-on: macos-26
     permissions:
     permissions:
       contents: write
       contents: write
     if:
     if:
@@ -175,10 +175,10 @@ jobs:
         (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
         (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
     steps:
     steps:
       - name: Select Xcode version
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_26.2.app/Contents/Developer"
       
       
       - name: Checkout Repo for building
       - name: Checkout Repo for building
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
         with:
           token: ${{ secrets.GH_PAT }}
           token: ${{ secrets.GH_PAT }}
           submodules: recursive
           submodules: recursive
@@ -258,7 +258,7 @@ jobs:
       # Upload Build artifacts
       # Upload Build artifacts
       - name: Upload build log, IPA and Symbol artifacts
       - name: Upload build log, IPA and Symbol artifacts
         if: always()
         if: always()
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v6
         with:
         with:
           name: build-artifacts
           name: build-artifacts
           path: |
           path: |

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

@@ -21,14 +21,14 @@ jobs:
   create_certs:
   create_certs:
     name: Certificates
     name: Certificates
     needs: validate
     needs: validate
-    runs-on: macos-15
+    runs-on: macos-26
     outputs:
     outputs:
       new_certificate_needed: ${{ steps.set_output.outputs.new_certificate_needed }}
       new_certificate_needed: ${{ steps.set_output.outputs.new_certificate_needed }}
 
 
     steps:
     steps:
       # Checks-out the repo
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
 
 
       # Patch Fastlane Match to not print tables
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
       - name: Patch Match Tables
@@ -90,14 +90,14 @@ jobs:
   nuke_certs:
   nuke_certs:
       name: Nuke certificates
       name: Nuke certificates
       needs: [validate, create_certs]
       needs: [validate, create_certs]
-      runs-on: macos-15
+      runs-on: macos-26
       if: ${{ (needs.create_certs.outputs.new_certificate_needed == 'true' && vars.ENABLE_NUKE_CERTS == 'true') || vars.FORCE_NUKE_CERTS == 'true' }}
       if: ${{ (needs.create_certs.outputs.new_certificate_needed == 'true' && vars.ENABLE_NUKE_CERTS == 'true') || vars.FORCE_NUKE_CERTS == 'true' }}
       steps:
       steps:
         - name: Output from step id 'check_certs'
         - name: Output from step id 'check_certs'
           run: echo "new_certificate_needed=${{ needs.create_certs.outputs.new_certificate_needed }}"
           run: echo "new_certificate_needed=${{ needs.create_certs.outputs.new_certificate_needed }}"
 
 
         - name: Checkout repository
         - name: Checkout repository
-          uses: actions/checkout@v4
+          uses: actions/checkout@v5
 
 
         - name: Install dependencies
         - name: Install dependencies
           run: bundle install
           run: bundle install

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

@@ -11,7 +11,7 @@ jobs:
       pull-requests: write
       pull-requests: write
     if: github.repository_owner == 'nightscout'
     if: github.repository_owner == 'nightscout'
     steps:
     steps:
-      - uses: actions/stale@v9.0.0
+      - uses: actions/stale@v10
         with:
         with:
           days-before-issue-stale: 30
           days-before-issue-stale: 30
           days-before-issue-close: 14
           days-before-issue-close: 14
@@ -32,7 +32,7 @@ jobs:
       pull-requests: write
       pull-requests: write
     if: github.repository_owner == 'nightscout'
     if: github.repository_owner == 'nightscout'
     steps:
     steps:
-      - uses: actions/stale@v9.0.0
+      - uses: actions/stale@v10
         with:
         with:
           days-before-issue-stale: 30
           days-before-issue-stale: 30
           days-before-issue-close: 30
           days-before-issue-close: 30

+ 7 - 7
.github/workflows/unit_tests.yml

@@ -23,22 +23,22 @@ on:
 jobs:
 jobs:
   test:
   test:
     name: Run Unit Tests
     name: Run Unit Tests
-    runs-on: macos-15
+    runs-on: macos-26
     if: github.repository_owner == 'nightscout'
     if: github.repository_owner == 'nightscout'
 
 
     steps:
     steps:
       - name: Select Xcode version
       - name: Select Xcode version
-        run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer
+        run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
 
 
       - name: Checkout code
       - name: Checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
         with:
         with:
           fetch-depth: 1
           fetch-depth: 1
           submodules: recursive
           submodules: recursive
 
 
       - name: Restore cache
       - name: Restore cache
         id: cache-restore
         id: cache-restore
-        uses: actions/cache/restore@v4
+        uses: actions/cache/restore@v5
         with:
         with:
           path: |
           path: |
             /Users/runner/Library/Developer/Xcode/DerivedData
             /Users/runner/Library/Developer/Xcode/DerivedData
@@ -64,7 +64,7 @@ jobs:
           time xcodebuild build-for-testing \
           time xcodebuild build-for-testing \
             -workspace Trio.xcworkspace \
             -workspace Trio.xcworkspace \
             -scheme "Trio Tests" \
             -scheme "Trio Tests" \
-            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' \
+            -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
 
 
       - name: Check for uncommitted changes
       - name: Check for uncommitted changes
         run: |
         run: |
@@ -94,7 +94,7 @@ jobs:
           
           
       - name: Save cache
       - name: Save cache
         if: steps.cache-restore.outputs.cache-hit != 'true'
         if: steps.cache-restore.outputs.cache-hit != 'true'
-        uses: actions/cache/save@v4
+        uses: actions/cache/save@v5
         with:
         with:
           path: |
           path: |
             /Users/runner/Library/Developer/Xcode/DerivedData
             /Users/runner/Library/Developer/Xcode/DerivedData
@@ -107,7 +107,7 @@ jobs:
           time xcodebuild test-without-building \
           time xcodebuild test-without-building \
             -workspace Trio.xcworkspace \
             -workspace Trio.xcworkspace \
             -scheme "Trio Tests" \
             -scheme "Trio Tests" \
-            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' \
+            -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' \
             $([ "$ENABLE_PARALLEL_TESTING" = "true" ] && echo "-parallel-testing-enabled YES") \
             $([ "$ENABLE_PARALLEL_TESTING" = "true" ] && echo "-parallel-testing-enabled YES") \
             2>&1 | tee xcodebuild.log
             2>&1 | tee xcodebuild.log
 
 

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

@@ -105,7 +105,7 @@ jobs:
   validate-fastlane-secrets:
   validate-fastlane-secrets:
     name: Fastlane
     name: Fastlane
     needs: [validate-access-token]
     needs: [validate-access-token]
-    runs-on: macos-15
+    runs-on: macos-26
     env:
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -116,7 +116,7 @@ jobs:
       TEAMID: ${{ secrets.TEAMID }}
       TEAMID: ${{ secrets.TEAMID }}
     steps:
     steps:
       - name: Checkout Repo
       - name: Checkout Repo
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
 
 
       - name: Install Project Dependencies
       - name: Install Project Dependencies
         run: bundle install
         run: bundle install

+ 4 - 0
.gitmodules

@@ -41,6 +41,10 @@
 [submodule "DanaKit"]
 [submodule "DanaKit"]
 	path = DanaKit
 	path = DanaKit
 	url = https://github.com/loopandlearn/DanaKit
 	url = https://github.com/loopandlearn/DanaKit
+[submodule "MedtrumKit"]
+	path = MedtrumKit
+	branch = dev
+	url = https://github.com/loopandlearn/MedtrumKit
 [submodule "EversenseKit"]
 [submodule "EversenseKit"]
 	path = EversenseKit
 	path = EversenseKit
 	url = https://github.com/loopandlearn/EversenseKit
 	url = https://github.com/loopandlearn/EversenseKit

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 
 // The developers set the version numbers, please leave them alone
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.6.0
 APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.52
+APP_DEV_VERSION = 0.6.0.78
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 
 

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 3970b2aadc55044c851130567879fd0ae3ade6cc
+Subproject commit 0158fc85391725bb1855ea34469d48cb65667850

+ 1 - 1
LoopKit

@@ -1 +1 @@
-Subproject commit 9c09a6fea98e2638d76d610ba097c4fae14ca220
+Subproject commit 0229bb18d30b095420aae1e8fa04c37794e0a378

+ 1 - 0
MedtrumKit

@@ -0,0 +1 @@
+Subproject commit b7f3d44c06bb7c580be897e0414e64de2d6dd995

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

@@ -11,9 +11,10 @@ public extension GlucoseStored {
     @NSManaged var glucose: Int16
     @NSManaged var glucose: Int16
     @NSManaged var id: UUID?
     @NSManaged var id: UUID?
     @NSManaged var isManual: Bool
     @NSManaged var isManual: Bool
-    @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToHealth: Bool
     @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var isUploadedToTidepool: Bool
+    @NSManaged var smoothedGlucose: NSDecimalNumber?
 }
 }
 
 
 extension GlucoseStored: Identifiable {}
 extension GlucoseStored: Identifiable {}

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

@@ -84,6 +84,11 @@ extension NSPredicate {
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
     }
     }
 
 
+    static var pumpHistoryLast48h: NSPredicate {
+        let date = Date() - TimeInterval(hours: 48)
+        return NSPredicate(format: "timestamp >= %@", date as NSDate)
+    }
+
     static var pumpHistoryLast24h: NSPredicate {
     static var pumpHistoryLast24h: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
         return NSPredicate(format: "timestamp >= %@", date as NSDate)

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

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="25D2128" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -78,6 +78,7 @@
         <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="smoothedGlucose" optional="YES" attributeType="Decimal"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="ascending"/>
             <fetchIndexElement property="date" type="Binary" order="ascending"/>
         </fetchIndex>
         </fetchIndex>

+ 66 - 0
Trio Watch App Extension/Helper/WatchNotificationHandler.swift

@@ -0,0 +1,66 @@
+import Foundation
+import UserNotifications
+import WatchConnectivity
+
+final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate {
+    static let shared = WatchNotificationHandler()
+
+    override private init() {
+        super.init()
+    }
+
+    func configure() {
+        let center = UNUserNotificationCenter.current()
+        center.delegate = self
+        registerCategories(on: center)
+    }
+
+    private func registerCategories(on center: UNUserNotificationCenter) {
+        center.getNotificationCategories { existingCategories in
+            let glucoseCategory = NotificationCategoryFactory.createGlucoseCategory()
+
+            var categories = existingCategories
+            categories.update(with: glucoseCategory)
+            // UNUserNotificationCenter methods should be called on main thread
+            Task { @MainActor in
+                center.setNotificationCategories(categories)
+            }
+        }
+    }
+
+    /// UNUserNotificationCenterDelegate method called when user interacts with a notification on watch.
+    /// This can be called off the main thread. WCSession.transferUserInfo is thread-safe.
+    func userNotificationCenter(
+        _: UNUserNotificationCenter,
+        didReceive response: UNNotificationResponse,
+        withCompletionHandler completionHandler: @escaping () -> Void
+    ) {
+        defer { completionHandler() }
+
+        guard let action = NotificationResponseAction(rawValue: response.actionIdentifier) else { return }
+        sendSnoozeRequest(for: action)
+    }
+
+    /// Sends snooze request to iPhone via WatchConnectivity.
+    /// WCSession.transferUserInfo is thread-safe and can be called from any thread.
+    /// Relies on the watch app's WCSession owner (e.g., WatchState) to handle
+    /// session activation and delegate management.
+    private func sendSnoozeRequest(for action: NotificationResponseAction) {
+        guard WCSession.isSupported() else { return }
+
+        let payload: [String: Any] = [WatchMessageKeys.snoozeDuration: action.minutes]
+        let session = WCSession.default
+
+        // Try sendMessage first if session is reachable and activated (faster, immediate delivery)
+        // Fall back to transferUserInfo if not reachable or if sendMessage fails
+        if session.isReachable, session.activationState == .activated {
+            session.sendMessage(payload, replyHandler: nil) { _ in
+                // Fallback to transferUserInfo if sendMessage fails
+                session.transferUserInfo(payload)
+            }
+        } else {
+            // Session not reachable or not activated - use transferUserInfo (queued delivery)
+            session.transferUserInfo(payload)
+        }
+    }
+}

+ 5 - 0
Trio Watch App Extension/TrioWatchApp.swift

@@ -1,8 +1,13 @@
 import SwiftUI
 import SwiftUI
+import UserNotifications
 
 
 @main struct TrioWatchApp: App {
 @main struct TrioWatchApp: App {
     @Environment(\.scenePhase) private var scenePhase
     @Environment(\.scenePhase) private var scenePhase
 
 
+    init() {
+        WatchNotificationHandler.shared.configure()
+    }
+
     var body: some Scene {
     var body: some Scene {
         WindowGroup {
         WindowGroup {
             TrioMainWatchView()
             TrioMainWatchView()

+ 94 - 35
Trio.xcodeproj/project.pbxproj

@@ -260,16 +260,22 @@
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		3BF85FE32E427312000D7351 /* IOBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF85FE12E427312000D7351 /* IOBService.swift */; };
 		3BF85FE32E427312000D7351 /* IOBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF85FE12E427312000D7351 /* IOBService.swift */; };
 		3E28F2AB2EB5337F00FB9EEB /* ConnectIQ in Frameworks */ = {isa = PBXBuildFile; productRef = 3E28F2AA2EB5337F00FB9EEB /* ConnectIQ */; };
 		3E28F2AB2EB5337F00FB9EEB /* ConnectIQ in Frameworks */ = {isa = PBXBuildFile; productRef = 3E28F2AA2EB5337F00FB9EEB /* ConnectIQ */; };
+		3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; };
+		3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3E62C7822F54CC1B00433237 /* BolusDisplayThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */; };
 		3E84DA402F48D96000033608 /* EversenseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E84DA3F2F48D96000033608 /* EversenseKit.framework */; };
 		3E84DA402F48D96000033608 /* EversenseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E84DA3F2F48D96000033608 /* EversenseKit.framework */; };
 		3E84DA412F48D96000033608 /* EversenseKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E84DA3F2F48D96000033608 /* EversenseKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3E84DA412F48D96000033608 /* EversenseKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E84DA3F2F48D96000033608 /* EversenseKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
+		49090A8D2E9FE8D200D0F5DB /* GarminWatchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
 		49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49239B422EEA27AD00469145 /* TempTargetCalculations.swift */; };
 		49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49239B422EEA27AD00469145 /* TempTargetCalculations.swift */; };
+		4984D1D42EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
+		49C782A72F73D9870062B0DD /* AlertEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C782A62F73D9870062B0DD /* AlertEntry.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
 		581516A42BCED84A00BF67D7 /* DebuggingIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */; };
 		581516A42BCED84A00BF67D7 /* DebuggingIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */; };
@@ -462,6 +468,9 @@
 		C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */; };
 		C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */; };
 		C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */; };
 		C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */; };
 		C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */; };
 		C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */; };
+		C2BA6B972F758E7500348E6A /* WatchNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BA6B962F758E7500348E6A /* WatchNotificationHandler.swift */; };
+		C2BA6B992F758E7600348E6A /* NotificationIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */; };
+		C2BA6B9A2F7593C300348E6A /* NotificationIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
@@ -487,7 +496,6 @@
 		CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA34D2A064973004BE681 /* StateIntentRequest.swift */; };
 		CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA34D2A064973004BE681 /* StateIntentRequest.swift */; };
 		CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3572A064E2F004BE681 /* ListStateView.swift */; };
 		CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3572A064E2F004BE681 /* ListStateView.swift */; };
 		CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02428E867BA00473A9C /* AlertStorage.swift */; };
 		CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02428E867BA00473A9C /* AlertStorage.swift */; };
-		CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02628E869DF00473A9C /* AlertEntry.swift */; };
 		CE94597E29E9E1EE0047C9C6 /* GarminManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */; };
 		CE94597E29E9E1EE0047C9C6 /* GarminManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */; };
 		CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */; };
 		CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */; };
 		CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598129E9E3D30047C9C6 /* WatchConfigProvider.swift */; };
 		CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598129E9E3D30047C9C6 /* WatchConfigProvider.swift */; };
@@ -505,7 +513,6 @@
 		CE95BF622BA7715900DC3DE3 /* MockKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA4F274C26A300843DB3 /* MockKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE95BF622BA7715900DC3DE3 /* MockKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA4F274C26A300843DB3 /* MockKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE95BF632BA771BE00DC3DE3 /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; };
 		CE95BF632BA771BE00DC3DE3 /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; };
 		CE95BF642BA771BE00DC3DE3 /* LoopTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE95BF642BA771BE00DC3DE3 /* LoopTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3818AA70274C278200843DB3 /* LoopTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
-		CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */; };
 		CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */; };
 		CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */; };
 		CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E428B8FF5D00B70274 /* UIColor.swift */; };
 		CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E428B8FF5D00B70274 /* UIColor.swift */; };
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
 		CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */; };
@@ -625,6 +632,7 @@
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
+		DDA40BBA2F4DB18800257798 /* AlgorithmGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */; };
 		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.swift */; };
 		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.swift */; };
 		DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E2842D2361F800C2988C /* LoopStatusView.swift */; };
 		DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E2842D2361F800C2988C /* LoopStatusView.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
@@ -654,6 +662,10 @@
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
 		DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD82DC421B500AC63F3 /* suggested.json */; };
 		DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD82DC421B500AC63F3 /* suggested.json */; };
+		DDD7C8C12F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */; };
+		DDD7C8C22F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */; };
+		DDDD0FFB2F4E22C000F9C645 /* GlucoseSmoothingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD0FFA2F4E22C000F9C645 /* GlucoseSmoothingTests.swift */; };
+		DDDD0FFF2F4E231B00F9C645 /* MockTDDStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDD0FFE2F4E231B00F9C645 /* MockTDDStorage.swift */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
@@ -670,8 +682,6 @@
 		DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */; };
 		DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */; };
 		DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */; };
 		DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */; };
 		DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */; };
 		DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */; };
-		DDE179642C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */; };
-		DDE179652C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */; };
 		DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */; };
 		DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */; };
 		DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */; };
 		DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */; };
 		DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */; };
 		DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */; };
@@ -789,6 +799,7 @@
 				3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */,
 				3B4BA7792D8DBD690069D5B8 /* MinimedKit.framework in Embed Frameworks */,
 				CE95BF5C2BA770C300DC3DE3 /* LoopKit.framework in Embed Frameworks */,
 				CE95BF5C2BA770C300DC3DE3 /* LoopKit.framework in Embed Frameworks */,
 				3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */,
 				3B4BA7712D8DBD690069D5B8 /* G7SensorKit.framework in Embed Frameworks */,
+				3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */,
 				CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */,
 				CEB434FE28B90B8C00B70274 /* SwiftCharts in Embed Frameworks */,
 				3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */,
 				3B4BA7812D8DBD690069D5B8 /* OmniKitUI.framework in Embed Frameworks */,
 				3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */,
 				3B4BA76F2D8DBD690069D5B8 /* DanaKit.framework in Embed Frameworks */,
@@ -873,7 +884,6 @@
 		1967DFBD29D052C200759F30 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
 		1967DFBD29D052C200759F30 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
 		1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = "<group>"; };
 		1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = "<group>"; };
 		1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
 		1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
-		199561C0275E61A50077B976 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS8.0.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
 		19A910352A24D6D700C8951B /* DateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFilter.swift; sourceTree = "<group>"; };
 		19A910352A24D6D700C8951B /* DateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFilter.swift; sourceTree = "<group>"; };
 		19B0EF2028F6D66200069496 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
 		19B0EF2028F6D66200069496 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
 		19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealSettingsDataFlow.swift; sourceTree = "<group>"; };
 		19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealSettingsDataFlow.swift; sourceTree = "<group>"; };
@@ -1093,17 +1103,22 @@
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3BF85FE12E427312000D7351 /* IOBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBService.swift; sourceTree = "<group>"; };
 		3BF85FE12E427312000D7351 /* IOBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBService.swift; sourceTree = "<group>"; };
+		3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MedtrumKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusDisplayThreshold.swift; sourceTree = "<group>"; };
 		3E84DA3F2F48D96000033608 /* EversenseKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EversenseKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3E84DA3F2F48D96000033608 /* EversenseKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EversenseKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
 		3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
 		3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
 		42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
 		42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
 		44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorProvider.swift; sourceTree = "<group>"; };
 		44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorProvider.swift; sourceTree = "<group>"; };
+		49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchSettings.swift; sourceTree = "<group>"; };
 		491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		49239B422EEA27AD00469145 /* TempTargetCalculations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetCalculations.swift; sourceTree = "<group>"; };
 		49239B422EEA27AD00469145 /* TempTargetCalculations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetCalculations.swift; sourceTree = "<group>"; };
+		4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigGarminAppConfigView.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
+		49C782A62F73D9870062B0DD /* AlertEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertEntry.swift; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingIdentifiers.swift; sourceTree = "<group>"; };
 		581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingIdentifiers.swift; sourceTree = "<group>"; };
@@ -1296,6 +1311,8 @@
 		C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportDataFlow.swift; sourceTree = "<group>"; };
 		C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportDataFlow.swift; sourceTree = "<group>"; };
 		C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportProvider.swift; sourceTree = "<group>"; };
 		C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportProvider.swift; sourceTree = "<group>"; };
 		C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportStateModel.swift; sourceTree = "<group>"; };
 		C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportStateModel.swift; sourceTree = "<group>"; };
+		C2BA6B962F758E7500348E6A /* WatchNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNotificationHandler.swift; sourceTree = "<group>"; };
+		C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationIdentifiers.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
@@ -1328,7 +1345,6 @@
 		CE7CA34D2A064973004BE681 /* StateIntentRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateIntentRequest.swift; sourceTree = "<group>"; };
 		CE7CA34D2A064973004BE681 /* StateIntentRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateIntentRequest.swift; sourceTree = "<group>"; };
 		CE7CA3572A064E2F004BE681 /* ListStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStateView.swift; sourceTree = "<group>"; };
 		CE7CA3572A064E2F004BE681 /* ListStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStateView.swift; sourceTree = "<group>"; };
 		CE82E02428E867BA00473A9C /* AlertStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStorage.swift; sourceTree = "<group>"; };
 		CE82E02428E867BA00473A9C /* AlertStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStorage.swift; sourceTree = "<group>"; };
-		CE82E02628E869DF00473A9C /* AlertEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertEntry.swift; sourceTree = "<group>"; };
 		CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ConnectIQ.framework; path = "Dependencies/ios-armv7_arm64/ConnectIQ.framework"; sourceTree = "<group>"; };
 		CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ConnectIQ.framework; path = "Dependencies/ios-armv7_arm64/ConnectIQ.framework"; sourceTree = "<group>"; };
 		CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminManager.swift; sourceTree = "<group>"; };
 		CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminManager.swift; sourceTree = "<group>"; };
 		CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigDataFlow.swift; sourceTree = "<group>"; };
 		CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1339,7 +1355,6 @@
 		CE95BF4A2BA5CED700DC3DE3 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE95BF4A2BA5CED700DC3DE3 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE95BF562BA5F5FE00DC3DE3 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
 		CE95BF562BA5F5FE00DC3DE3 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
 		CE95BF592BA62E4A00DC3DE3 /* PluginSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginSource.swift; sourceTree = "<group>"; };
 		CE95BF592BA62E4A00DC3DE3 /* PluginSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginSource.swift; sourceTree = "<group>"; };
-		CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavitzkyGolayFilter.swift; sourceTree = "<group>"; };
 		CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DE28B8F5C400B70274 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DE28B8F5C400B70274 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
 		CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
@@ -1461,6 +1476,7 @@
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
+		DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmGlucose.swift; sourceTree = "<group>"; };
 		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.swift; sourceTree = "<group>"; };
 		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.swift; sourceTree = "<group>"; };
 		DDA6E2842D2361F800C2988C /* LoopStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusView.swift; sourceTree = "<group>"; };
 		DDA6E2842D2361F800C2988C /* LoopStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusView.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
@@ -1493,6 +1509,10 @@
 		DDD78A902DC4064800AC63F3 /* carbhistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = carbhistory.json; sourceTree = "<group>"; };
 		DDD78A902DC4064800AC63F3 /* carbhistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = carbhistory.json; sourceTree = "<group>"; };
 		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
 		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
 		DDD78AD82DC421B500AC63F3 /* suggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested.json; sourceTree = "<group>"; };
 		DDD78AD82DC421B500AC63F3 /* suggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested.json; sourceTree = "<group>"; };
+		DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
+		DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		DDDD0FFA2F4E22C000F9C645 /* GlucoseSmoothingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSmoothingTests.swift; sourceTree = "<group>"; };
+		DDDD0FFE2F4E231B00F9C645 /* MockTDDStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTDDStorage.swift; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1509,8 +1529,6 @@
 		DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsData+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsData+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Forecast+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
-		DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1590,6 +1608,7 @@
 				3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */,
 				3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */,
 				3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */,
 				3B4BA7782D8DBD690069D5B8 /* MinimedKit.framework in Frameworks */,
 				3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */,
 				3B4BA7762D8DBD690069D5B8 /* LibreTransmitterUI.framework in Frameworks */,
+				3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */,
 				3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */,
 				3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */,
 				3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */,
 				3B4BA76A2D8DBD690069D5B8 /* CGMBLEKit.framework in Frameworks */,
 				3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */,
 				3B4BA77C2D8DBD690069D5B8 /* OmniBLE.framework in Frameworks */,
@@ -1774,7 +1793,6 @@
 		192F0FF5276AC36D0085BE4D /* Recovered References */ = {
 		192F0FF5276AC36D0085BE4D /* Recovered References */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				199561C0275E61A50077B976 /* HealthKit.framework */,
 			);
 			);
 			name = "Recovered References";
 			name = "Recovered References";
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2196,6 +2214,7 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */,
 				3E84DA3F2F48D96000033608 /* EversenseKit.framework */,
 				3E84DA3F2F48D96000033608 /* EversenseKit.framework */,
 				3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,
 				3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,
 				3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */,
 				3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */,
@@ -2375,20 +2394,9 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				DD3D60302F0377350021A33B /* ExportSetting.swift */,
-				DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */,
-				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
-				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
-				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
-				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
-				DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
-				BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */,
-				BD54A9722D281A9C00F9C1EE /* TempTargetPresetWatch.swift */,
-				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
-				BDA25EFC2D261BF200035F34 /* WatchState.swift */,
-				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
-				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
-				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
+				49C782A62F73D9870062B0DD /* AlertEntry.swift */,
+				DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */,
+				3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
 				38D0B3B525EBE24900CB6E88 /* Battery.swift */,
 				38D0B3B525EBE24900CB6E88 /* Battery.swift */,
@@ -2396,24 +2404,55 @@
 				3870FF4225EC13F40088248F /* BloodGlucose.swift */,
 				3870FF4225EC13F40088248F /* BloodGlucose.swift */,
 				38A9260425F012D8009E3739 /* CarbRatios.swift */,
 				38A9260425F012D8009E3739 /* CarbRatios.swift */,
 				38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */,
 				38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */,
-				3811DF0125CA9FEA00A708ED /* Credentials.swift */,
+				19D4E4EA29FC6A9F00351451 /* Charts.swift */,
+				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
+				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */,
 				E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */,
-				38AEE73C25F0200C0013F05B /* TrioSettings.swift */,
+				3811DF0125CA9FEA00A708ED /* Credentials.swift */,
+				19A910352A24D6D700C8951B /* DateFilter.swift */,
+				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
+				583684072BD195A700070A60 /* Determination.swift */,
+				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
+				DD3D60302F0377350021A33B /* ExportSetting.swift */,
+				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
+				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
+				49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */,
+				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
+				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
+				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
+				E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */,
+				1967DFBD29D052C200759F30 /* Icons.swift */,
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 				38887CCD25F5725200944304 /* IOBEntry.swift */,
 				38887CCD25F5725200944304 /* IOBEntry.swift */,
+				BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */,
+				193F6CDC2A512C8F001240FD /* Loops.swift */,
+				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
 				DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */,
 				DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */,
+				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
+				191F62672AD6B05A004D7911 /* NightscoutSettings.swift */,
 				385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */,
 				385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */,
 				389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */,
 				389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */,
+				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
+				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
 				3895E4C525B9E00D00214B37 /* Preferences.swift */,
 				3895E4C525B9E00D00214B37 /* Preferences.swift */,
 				38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */,
 				38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */,
 				3883583325EEB38000E024B2 /* PumpSettings.swift */,
 				3883583325EEB38000E024B2 /* PumpSettings.swift */,
 				38E989DC25F5021400C0CED0 /* PumpStatus.swift */,
 				38E989DC25F5021400C0CED0 /* PumpStatus.swift */,
+				CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
 				38BF021C25E7E3AF00579895 /* Reservoir.swift */,
 				38BF021C25E7E3AF00579895 /* Reservoir.swift */,
+				19B0EF2028F6D66200069496 /* Statistics.swift */,
+				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
-				3811DE8E25C9D80400A708ED /* User.swift */,
-				E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */,
+				BD54A9722D281A9C00F9C1EE /* TempTargetPresetWatch.swift */,
+				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
 				1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */,
 				1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */,
+				38AEE73C25F0200C0013F05B /* TrioSettings.swift */,
+				3811DE8E25C9D80400A708ED /* User.swift */,
+				DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
+				BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */,
+				BDA25EFC2D261BF200035F34 /* WatchState.swift */,
+				DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */,
 				CE82E02628E869DF00473A9C /* AlertEntry.swift */,
 				CE82E02628E869DF00473A9C /* AlertEntry.swift */,
 				19B0EF2028F6D66200069496 /* Statistics.swift */,
 				19B0EF2028F6D66200069496 /* Statistics.swift */,
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
@@ -2431,6 +2470,7 @@
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				DD9ECB692CA99F6C00AA7C45 /* CommandPayload.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
+				C2BA6B982F758E7600348E6A /* NotificationIdentifiers.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2469,7 +2509,6 @@
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
 				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
 				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
-				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
 				49239B422EEA27AD00469145 /* TempTargetCalculations.swift */,
 				49239B422EEA27AD00469145 /* TempTargetCalculations.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
@@ -2614,6 +2653,8 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDDD0FFD2F4E231600F9C645 /* Mocks */,
+				DDDD0FFA2F4E22C000F9C645 /* GlucoseSmoothingTests.swift */,
 				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
@@ -3163,6 +3204,7 @@
 		CE94598529E9E3FE0047C9C6 /* View */ = {
 		CE94598529E9E3FE0047C9C6 /* View */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */,
 				CE94598629E9E4110047C9C6 /* WatchConfigRootView.swift */,
 				CE94598629E9E4110047C9C6 /* WatchConfigRootView.swift */,
 				DDF847E72C5DABA30049BB3B /* WatchConfigAppleWatchView.swift */,
 				DDF847E72C5DABA30049BB3B /* WatchConfigAppleWatchView.swift */,
 				DDF847E92C5DABAC0049BB3B /* WatchConfigGarminView.swift */,
 				DDF847E92C5DABAC0049BB3B /* WatchConfigGarminView.swift */,
@@ -3381,6 +3423,7 @@
 				DD09D5C62D29EB26000D82C9 /* Helper+Enums.swift */,
 				DD09D5C62D29EB26000D82C9 /* Helper+Enums.swift */,
 				DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */,
 				DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */,
 				DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */,
 				DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */,
+				C2BA6B962F758E7500348E6A /* WatchNotificationHandler.swift */,
 			);
 			);
 			path = Helper;
 			path = Helper;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -3583,9 +3626,19 @@
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		DDDD0FFD2F4E231600F9C645 /* Mocks */ = {
+			isa = PBXGroup;
+			children = (
+				DDDD0FFE2F4E231B00F9C645 /* MockTDDStorage.swift */,
+			);
+			path = Mocks;
+			sourceTree = "<group>";
+		};
 		DDE179112C9100FA003CDDB7 /* Classes+Properties */ = {
 		DDE179112C9100FA003CDDB7 /* Classes+Properties */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDD7C8BF2F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift */,
+				DDD7C8C02F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift */,
 				BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */,
 				BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */,
 				BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */,
 				BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */,
 				DDE179362C910127003CDDB7 /* BolusStored+CoreDataClass.swift */,
 				DDE179362C910127003CDDB7 /* BolusStored+CoreDataClass.swift */,
@@ -3600,8 +3653,6 @@
 				DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */,
 				DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */,
 				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
 				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
 				DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */,
 				DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */,
-				DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */,
-				DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */,
 				DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */,
 				DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */,
 				DDE179352C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift */,
 				DDE179352C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift */,
 				DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */,
 				DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */,
@@ -4153,6 +4204,7 @@
 			isa = PBXSourcesBuildPhase;
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				DDA40BBA2F4DB18800257798 /* AlgorithmGlucose.swift in Sources */,
 				DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */,
 				DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */,
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
 				C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */,
 				C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */,
@@ -4181,7 +4233,6 @@
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				BD47FDD92D8B657D0043966B /* InsulinSensitivityStepView.swift in Sources */,
 				BD47FDD92D8B657D0043966B /* InsulinSensitivityStepView.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
-				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
 				58D08B322C8DF88900AA37D3 /* DummyCharts.swift in Sources */,
 				58D08B322C8DF88900AA37D3 /* DummyCharts.swift in Sources */,
@@ -4262,6 +4313,7 @@
 				BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */,
 				BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */,
 				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
+				4984D1D42EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */,
 				DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
@@ -4310,6 +4362,7 @@
 				DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */,
 				DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
+				49090A8D2E9FE8D200D0F5DB /* GarminWatchSettings.swift in Sources */,
 				49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */,
 				49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */,
 				DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */,
@@ -4366,6 +4419,7 @@
 				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
 				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
+				3E62C7822F54CC1B00433237 /* BolusDisplayThreshold.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
@@ -4493,6 +4547,7 @@
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */,
 				DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */,
+				C2BA6B992F758E7600348E6A /* NotificationIdentifiers.swift in Sources */,
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
 				CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */,
 				CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
@@ -4522,6 +4577,8 @@
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
 				DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */,
 				DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */,
+				DDD7C8C12F4DB45400E5CF09 /* GlucoseStored+CoreDataClass.swift in Sources */,
+				DDD7C8C22F4DB45400E5CF09 /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
@@ -4611,7 +4668,6 @@
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
 				BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */,
 				BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
-				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
 				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				BD2FF1A02AE29D43005D1C5D /* ToggleStyles.swift in Sources */,
 				BD2FF1A02AE29D43005D1C5D /* ToggleStyles.swift in Sources */,
@@ -4717,14 +4773,13 @@
 				DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */,
 				DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */,
 				DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */,
 				DDE179622C910127003CDDB7 /* Forecast+CoreDataClass.swift in Sources */,
 				DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */,
 				DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */,
-				DDE179642C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift in Sources */,
-				DDE179652C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift in Sources */,
 				BDC531142D10611D00088832 /* AddContactImageSheet.swift in Sources */,
 				BDC531142D10611D00088832 /* AddContactImageSheet.swift in Sources */,
 				DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,
 				DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,
 				DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DDE179692C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift in Sources */,
 				DDE179692C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift in Sources */,
 				BD4D73A22D15A42A0052227B /* TDDStorage.swift in Sources */,
 				BD4D73A22D15A42A0052227B /* TDDStorage.swift in Sources */,
+				49C782A72F73D9870062B0DD /* AlertEntry.swift in Sources */,
 				DDE1796C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift in Sources */,
 				DDE1796C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift in Sources */,
 				DDE1796D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift in Sources */,
 				DDE1796D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift in Sources */,
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
@@ -4753,12 +4808,14 @@
 				BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */,
 				BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */,
 				BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */,
 				BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */,
 				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
 				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
+				DDDD0FFB2F4E22C000F9C645 /* GlucoseSmoothingTests.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
 				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,
 				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,
 				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
+				DDDD0FFF2F4E231B00F9C645 /* MockTDDStorage.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				3BAAE60C2DE7766C0049589B /* DynamicISFEnableTests.swift in Sources */,
 				3BAAE60C2DE7766C0049589B /* DynamicISFEnableTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
@@ -4796,6 +4853,7 @@
 				BDA25F202D26D5FE00035F34 /* CarbsInputView.swift in Sources */,
 				BDA25F202D26D5FE00035F34 /* CarbsInputView.swift in Sources */,
 				BDA25F1C2D26BD0700035F34 /* TrendShape.swift in Sources */,
 				BDA25F1C2D26BD0700035F34 /* TrendShape.swift in Sources */,
 				BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */,
 				BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */,
+				C2BA6B972F758E7500348E6A /* WatchNotificationHandler.swift in Sources */,
 				DD09D5C72D29EB2F000D82C9 /* Helper+Enums.swift in Sources */,
 				DD09D5C72D29EB2F000D82C9 /* Helper+Enums.swift in Sources */,
 				BD54A9742D281AEF00F9C1EE /* TempTargetPresetWatch.swift in Sources */,
 				BD54A9742D281AEF00F9C1EE /* TempTargetPresetWatch.swift in Sources */,
 				BD54A95C2D2808A300F9C1EE /* OverridePresetWatch.swift in Sources */,
 				BD54A95C2D2808A300F9C1EE /* OverridePresetWatch.swift in Sources */,
@@ -4806,6 +4864,7 @@
 				DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */,
 				DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */,
 				DD246F062D2836AA0027DDE0 /* GlucoseTrendView.swift in Sources */,
 				DD246F062D2836AA0027DDE0 /* GlucoseTrendView.swift in Sources */,
 				BD432CA22D2F4E4000D1EB79 /* WatchMessageKeys.swift in Sources */,
 				BD432CA22D2F4E4000D1EB79 /* WatchMessageKeys.swift in Sources */,
+				C2BA6B9A2F7593C300348E6A /* NotificationIdentifiers.swift in Sources */,
 				DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */,
 				DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */,
 				BD54A9712D281A8100F9C1EE /* TempTargetPresetsView.swift in Sources */,
 				BD54A9712D281A8100F9C1EE /* TempTargetPresetsView.swift in Sources */,
 				DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */,
 				DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */,

+ 3 - 0
Trio.xcworkspace/contents.xcworkspacedata

@@ -17,6 +17,9 @@
       location = "group:DanaKit/DanaKit.xcodeproj">
       location = "group:DanaKit/DanaKit.xcodeproj">
    </FileRef>
    </FileRef>
    <FileRef
    <FileRef
+      location = "group:MedtrumKit/MedtrumKit.xcodeproj">
+   </FileRef>
+   <FileRef
       location = "group:RileyLinkKit/RileyLinkKit.xcodeproj">
       location = "group:RileyLinkKit/RileyLinkKit.xcodeproj">
    </FileRef>
    </FileRef>
    <FileRef
    <FileRef

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

@@ -51,8 +51,8 @@
       "kind" : "remoteSourceControl",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/garmin/connectiq-companion-app-sdk-ios",
       "location" : "https://github.com/garmin/connectiq-companion-app-sdk-ios",
       "state" : {
       "state" : {
-        "revision" : "00594907c84884a9430c6a33825940c2769f261a",
-        "version" : "1.7.0"
+        "revision" : "f0d29ff691d700a132d86205ed9bb091e336c2f7",
+        "version" : "1.8.0"
       }
       }
     },
     },
     {
     {

+ 5 - 5
Trio/Resources/Info.plist

@@ -74,13 +74,13 @@
 	<key>NSBluetoothPeripheralUsageDescription</key>
 	<key>NSBluetoothPeripheralUsageDescription</key>
 	<string>Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices</string>
 	<string>Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices</string>
 	<key>NSCalendarsFullAccessUsageDescription</key>
 	<key>NSCalendarsFullAccessUsageDescription</key>
-	<string>To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay</string>
+	<string>To create events with glucose values, so they can be viewed on Apple Watch and CarPlay</string>
 	<key>NSCalendarsUsageDescription</key>
 	<key>NSCalendarsUsageDescription</key>
-	<string>Calendar is used to create a new glucose events.</string>
+	<string>Calendar is used to create new glucose events.</string>
 	<key>NSContactsUsageDescription</key>
 	<key>NSContactsUsageDescription</key>
 	<string>Contact is used to create a Apple Watch complication</string>
 	<string>Contact is used to create a Apple Watch complication</string>
 	<key>NSFaceIDUsageDescription</key>
 	<key>NSFaceIDUsageDescription</key>
-	<string>For authorized acces to bolus</string>
+	<string>For authorized access to bolus</string>
 	<key>NSHealthShareUsageDescription</key>
 	<key>NSHealthShareUsageDescription</key>
 	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<key>NSHealthUpdateUsageDescription</key>
 	<key>NSHealthUpdateUsageDescription</key>
@@ -107,8 +107,8 @@
 		<string>remote-notification</string>
 		<string>remote-notification</string>
 		<string>audio</string>
 		<string>audio</string>
 	</array>
 	</array>
-    <key>UIDesignRequiresCompatibility</key>
-    <true/>
+	<key>UIDesignRequiresCompatibility</key>
+	<true/>
 	<key>UIFileSharingEnabled</key>
 	<key>UIFileSharingEnabled</key>
 	<true/>
 	<true/>
 	<key>UILaunchScreen</key>
 	<key>UILaunchScreen</key>

+ 3 - 111
Trio/Resources/InfoPlist.xcstrings

@@ -464,7 +464,7 @@
         "en" : {
         "en" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "new",
             "state" : "new",
-            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
+            "value" : "To create events with glucose values, so they can be viewed on Apple Watch and CarPlay"
           }
           }
         }
         }
       }
       }
@@ -473,18 +473,6 @@
       "comment" : "Privacy - Calendars Usage Description",
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",
       "extractionState" : "extracted_with_value",
       "localizations" : {
       "localizations" : {
-        "ar" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
-        "ca" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
         "da" : {
         "da" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "translated",
             "state" : "translated",
@@ -500,19 +488,7 @@
         "en" : {
         "en" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "new",
             "state" : "new",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
-        "es" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
-        "fi" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
+            "value" : "Calendar is used to create new glucose events."
           }
           }
         },
         },
         "fr" : {
         "fr" : {
@@ -521,18 +497,6 @@
             "value" : "Le calendrier est utilisé pour créer un nouvel événement de glycémie."
             "value" : "Le calendrier est utilisé pour créer un nouvel événement de glycémie."
           }
           }
         },
         },
-        "he" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
-        "hu" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
         "it" : {
         "it" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "translated",
             "state" : "translated",
@@ -551,24 +515,6 @@
             "value" : "Agenda wordt gebruikt om nieuwe glucose gebeurtenissen aan te maken."
             "value" : "Agenda wordt gebruikt om nieuwe glucose gebeurtenissen aan te maken."
           }
           }
         },
         },
-        "pl" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
-        "pt-BR" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
-        "pt-PT" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Calendar is used to create a new glucose events."
-          }
-        },
         "ru" : {
         "ru" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "translated",
             "state" : "translated",
@@ -628,18 +574,6 @@
       "comment" : "Privacy - Face ID Usage Description",
       "comment" : "Privacy - Face ID Usage Description",
       "extractionState" : "extracted_with_value",
       "extractionState" : "extracted_with_value",
       "localizations" : {
       "localizations" : {
-        "ar" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
-        "ca" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
         "da" : {
         "da" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "translated",
             "state" : "translated",
@@ -655,19 +589,7 @@
         "en" : {
         "en" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "new",
             "state" : "new",
-            "value" : "For authorized acces to bolus"
-          }
-        },
-        "es" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
-        "fi" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
+            "value" : "For authorized access to bolus"
           }
           }
         },
         },
         "fr" : {
         "fr" : {
@@ -676,18 +598,6 @@
             "value" : "Pour les accès autorisés au bolus"
             "value" : "Pour les accès autorisés au bolus"
           }
           }
         },
         },
-        "he" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
-        "hu" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
         "it" : {
         "it" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "translated",
             "state" : "translated",
@@ -706,24 +616,6 @@
             "value" : "Voor geautoriseerde toegang tot bolus"
             "value" : "Voor geautoriseerde toegang tot bolus"
           }
           }
         },
         },
-        "pl" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
-        "pt-BR" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
-        "pt-PT" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "For authorized acces to bolus"
-          }
-        },
         "ru" : {
         "ru" : {
           "stringUnit" : {
           "stringUnit" : {
             "state" : "translated",
             "state" : "translated",

+ 8 - 3
Trio/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -19,9 +19,8 @@
   "highGlucose" : 270,
   "highGlucose" : 270,
   "carbsRequiredThreshold" : 10,
   "carbsRequiredThreshold" : 10,
   "showCarbsRequiredBadge" : true,
   "showCarbsRequiredBadge" : true,
-  "useFPUconversion" : true,
+  "useFPUconversion" : false,
   "individualAdjustmentFactor" : 0.5,
   "individualAdjustmentFactor" : 0.5,
-  "timeCap" : 8,
   "minuteInterval" : 30,
   "minuteInterval" : 30,
   "delay" : 60,
   "delay" : 60,
   "useAppleHealth" : false,
   "useAppleHealth" : false,
@@ -34,6 +33,7 @@
   "xGridLines" : true,
   "xGridLines" : true,
   "yGridLines" : true,
   "yGridLines" : true,
   "rulerMarks" : true,
   "rulerMarks" : true,
+  "bolusDisplayThreshold": 0.01,
   "forecastDisplayType": "cone",
   "forecastDisplayType": "cone",
   "maxCarbs": 250,
   "maxCarbs": 250,
   "maxFat": 250,
   "maxFat": 250,
@@ -48,5 +48,10 @@
   "useCalendar": false,
   "useCalendar": false,
   "displayCalendarIOBandCOB": false,
   "displayCalendarIOBandCOB": false,
   "displayCalendarEmojis": false,
   "displayCalendarEmojis": false,
-  "timeInRangeType": "timeInTightRange"
+  "timeInRangeType": "timeInTightRange",
+  "garminWatchface": "trio",
+  "garminDatafield": "none",
+  "primaryAttributeChoice": "cob",
+  "secondaryAttributeChoice": "tbr",
+  "isWatchfaceDataEnabled": true  
 }
 }

+ 12 - 2
Trio/Sources/APS/APSManager.swift

@@ -18,6 +18,7 @@ protocol APSManager {
     var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
     var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
     var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
     var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
+    var pumpActivatedAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     var isManualTempBasal: Bool { get }
     var isScheduledBasal: Bool? { get }
     var isScheduledBasal: Bool? { get }
     var isSuspended: Bool { get }
     var isSuspended: Bool { get }
@@ -129,6 +130,10 @@ final class BaseAPSManager: APSManager, Injectable {
         deviceDataManager.pumpExpiresAtDate
         deviceDataManager.pumpExpiresAtDate
     }
     }
 
 
+    var pumpActivatedAtDate: CurrentValueSubject<Date?, Never> {
+        deviceDataManager.pumpActivatedAtDate
+    }
+
     var settings: TrioSettings {
     var settings: TrioSettings {
         get { settingsManager.settings }
         get { settingsManager.settings }
         set { settingsManager.settings = newValue }
         set { settingsManager.settings = newValue }
@@ -405,7 +410,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard let autosense = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
         guard let autosense = await storage.retrieveAsync(OpenAPS.Settings.autosense, as: Autosens.self),
               (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
               (autosense.timestamp ?? .distantPast).addingTimeInterval(30.minutes.timeInterval) > Date()
         else {
         else {
-            let result = try await openAPS.autosense()
+            let result = try await openAPS.autosense(shouldSmoothGlucose: settingsManager.settings.smoothGlucose)
             return result != nil
             return result != nil
         }
         }
 
 
@@ -476,7 +481,11 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
             _ = try await autosenseResult
             _ = try await autosenseResult
             try await openAPS.createProfiles()
             try await openAPS.createProfiles()
-            let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
+            let determination = try await openAPS.determineBasal(
+                currentTemp: await currentTemp,
+                shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
+                clock: now
+            )
             iobFileDidUpdate.send(())
             iobFileDidUpdate.send(())
 
 
             guard isValidGlucoseData else {
             guard isValidGlucoseData else {
@@ -520,6 +529,7 @@ final class BaseAPSManager: APSManager, Injectable {
             let temp = try await fetchCurrentTempBasal(date: Date.now)
             let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
             return try await openAPS.determineBasal(
                 currentTemp: temp,
                 currentTemp: temp,
+                shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
                 clock: Date(),
                 clock: Date(),
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedBolusAmount: simulatedBolusAmount,
                 simulatedBolusAmount: simulatedBolusAmount,

+ 50 - 0
Trio/Sources/APS/DeviceDataManager.swift

@@ -5,6 +5,7 @@ import DanaKit
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
+import MedtrumKit
 import MinimedKit
 import MinimedKit
 import MockKit
 import MockKit
 import OmniBLE
 import OmniBLE
@@ -27,6 +28,7 @@ protocol DeviceDataManager: GlucoseSource {
     var errorSubject: PassthroughSubject<Error, Never> { get }
     var errorSubject: PassthroughSubject<Error, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
+    var pumpActivatedAtDate: CurrentValueSubject<Date?, Never> { get }
 
 
     func heartbeat(date: Date)
     func heartbeat(date: Date)
     func createBolusProgressReporter() -> DoseProgressReporter?
     func createBolusProgressReporter() -> DoseProgressReporter?
@@ -38,6 +40,7 @@ private let staticPumpManagers: [PumpManagerUI.Type] = [
     OmnipodPumpManager.self,
     OmnipodPumpManager.self,
     OmniBLEPumpManager.self,
     OmniBLEPumpManager.self,
     DanaKitPumpManager.self,
     DanaKitPumpManager.self,
+    MedtrumPumpManager.self,
     MockPumpManager.self
     MockPumpManager.self
 ]
 ]
 
 
@@ -46,6 +49,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
     OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self,
     OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self,
     OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self,
     OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self,
     DanaKitPumpManager.pluginIdentifier: DanaKitPumpManager.self,
     DanaKitPumpManager.pluginIdentifier: DanaKitPumpManager.self,
+    MedtrumPumpManager.pluginIdentifier: MedtrumPumpManager.self,
     MockPumpManager.pluginIdentifier: MockPumpManager.self
     MockPumpManager.pluginIdentifier: MockPumpManager.self
 ]
 ]
 
 
@@ -106,6 +110,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
 
 
                 if let omnipod = pumpManager as? OmnipodPumpManager {
                 if let omnipod = pumpManager as? OmnipodPumpManager {
+                    pumpActivatedAtDate.send(nil)
                     guard let endTime = omnipod.state.podState?.expiresAt else {
                     guard let endTime = omnipod.state.podState?.expiresAt else {
                         pumpExpiresAtDate.send(nil)
                         pumpExpiresAtDate.send(nil)
                         return
                         return
@@ -113,12 +118,30 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                     pumpExpiresAtDate.send(endTime)
                     pumpExpiresAtDate.send(endTime)
                 }
                 }
                 if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
                 if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
+                    pumpActivatedAtDate.send(nil)
                     guard let endTime = omnipodBLE.state.podState?.expiresAt else {
                     guard let endTime = omnipodBLE.state.podState?.expiresAt else {
                         pumpExpiresAtDate.send(nil)
                         pumpExpiresAtDate.send(nil)
                         return
                         return
                     }
                     }
                     pumpExpiresAtDate.send(endTime)
                     pumpExpiresAtDate.send(endTime)
                 }
                 }
+                if let medtrumPump = pumpManager as? MedtrumPumpManager {
+                    // Medtrum's state.patchExpiresAt is actually lifespan + grace
+                    // keeping this in line with omnipod, we will use just the lifetime
+                    // i.e., state.patchGracePeriodFrom
+                    guard let endTime = medtrumPump.state.patchGracePeriodFrom else {
+                        pumpExpiresAtDate.send(nil)
+                        return
+                    }
+                    pumpExpiresAtDate.send(endTime)
+
+                    switch medtrumPump.state.expiryMode {
+                    case .default:
+                        pumpActivatedAtDate.send(nil)
+                    case .extended:
+                        pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt)
+                    }
+                }
                 if let simulatorPump = pumpManager as? MockPumpManager {
                 if let simulatorPump = pumpManager as? MockPumpManager {
                     pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage)
                     pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage)
                     pumpName.send(simulatorPump.localizedTitle)
                     pumpName.send(simulatorPump.localizedTitle)
@@ -163,6 +186,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             } else {
             } else {
                 pumpDisplayState.value = nil
                 pumpDisplayState.value = nil
                 pumpExpiresAtDate.send(nil)
                 pumpExpiresAtDate.send(nil)
+                pumpActivatedAtDate.send(nil)
                 pumpName.send("")
                 pumpName.send("")
                 // Reset bolusIncrement setting to default value, which is 0.1 U
                 // Reset bolusIncrement setting to default value, which is 0.1 U
                 var modifiedPreferences = settingsManager.preferences
                 var modifiedPreferences = settingsManager.preferences
@@ -202,6 +226,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
 
     let pumpDisplayState = CurrentValueSubject<PumpDisplayState?, Never>(nil)
     let pumpDisplayState = CurrentValueSubject<PumpDisplayState?, Never>(nil)
     let pumpExpiresAtDate = CurrentValueSubject<Date?, Never>(nil)
     let pumpExpiresAtDate = CurrentValueSubject<Date?, Never>(nil)
+    let pumpActivatedAtDate = CurrentValueSubject<Date?, Never>(nil)
     let pumpName = CurrentValueSubject<String, Never>("Pump")
     let pumpName = CurrentValueSubject<String, Never>("Pump")
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
@@ -460,6 +485,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
                 manualTempBasal.send(false)
                 manualTempBasal.send(false)
             }
             }
 
 
+            pumpActivatedAtDate.send(nil)
             guard let endTime = omnipod.state.podState?.expiresAt else {
             guard let endTime = omnipod.state.podState?.expiresAt else {
                 pumpExpiresAtDate.send(nil)
                 pumpExpiresAtDate.send(nil)
                 return
                 return
@@ -493,6 +519,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
                 manualTempBasal.send(false)
                 manualTempBasal.send(false)
             }
             }
 
 
+            pumpActivatedAtDate.send(nil)
             guard let endTime = omnipodBLE.state.podState?.expiresAt else {
             guard let endTime = omnipodBLE.state.podState?.expiresAt else {
                 pumpExpiresAtDate.send(nil)
                 pumpExpiresAtDate.send(nil)
                 return
                 return
@@ -504,6 +531,29 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             }
             }
         }
         }
 
 
+        if let medtrumPump = pumpManager as? MedtrumPumpManager {
+            storage.save(Decimal(medtrumPump.state.reservoir), as: OpenAPS.Monitor.reservoir)
+            broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
+                $0.pumpReservoirDidChange(Decimal(medtrumPump.state.reservoir))
+            }
+
+            // Medtrum's state.patchExpiresAt is actually lifespan + grace
+            // keeping this in line with omnipod, we will use just the lifetime
+            // i.e., state.patchGracePeriodFrom
+            guard let endTime = medtrumPump.state.patchGracePeriodFrom else {
+                pumpExpiresAtDate.send(nil)
+                return
+            }
+            pumpExpiresAtDate.send(endTime)
+
+            switch medtrumPump.state.expiryMode {
+            case .default:
+                pumpActivatedAtDate.send(nil)
+            case .extended:
+                pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt)
+            }
+        }
+
         if let simulatorPump = pumpManager as? MockPumpManager {
         if let simulatorPump = pumpManager as? MockPumpManager {
             broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
             broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
                 $0.pumpReservoirDidChange(Decimal(simulatorPump.state.reservoirUnitsRemaining))
                 $0.pumpReservoirDidChange(Decimal(simulatorPump.state.reservoirUnitsRemaining))

+ 224 - 47
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -1,4 +1,5 @@
 import Combine
 import Combine
+import CoreData
 import Foundation
 import Foundation
 import HealthKit
 import HealthKit
 import LoopKit
 import LoopKit
@@ -29,6 +30,7 @@ extension FetchGlucoseManager {
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
 
 
+    @Injected() var broadcaster: Broadcaster!
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var tidepoolService: TidepoolManager!
     @Injected() var tidepoolService: TidepoolManager!
@@ -66,6 +68,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return cgmManager.shouldSyncToRemoteService
         return cgmManager.shouldSyncToRemoteService
     }
     }
 
 
+    var shouldSmoothGlucose: Bool = false
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         // init at the start of the app
         // init at the start of the app
@@ -76,6 +80,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             cgmGlucoseSourceType: settingsManager.settings.cgm,
             cgmGlucoseSourceType: settingsManager.settings.cgm,
             cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
             cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
         )
         )
+        shouldSmoothGlucose = settingsManager.settings.smoothGlucose
         subscribe()
         subscribe()
     }
     }
 
 
@@ -117,6 +122,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             .store(in: &lifetime)
             .store(in: &lifetime)
         timer.fire()
         timer.fire()
         timer.resume()
         timer.resume()
+
+        broadcaster.register(SettingsObserver.self, observer: self)
     }
     }
 
 
     /// Store new glucose readings from the CGM manager
     /// Store new glucose readings from the CGM manager
@@ -197,7 +204,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager {
         if let manager = newManager {
             cgmManager = manager
             cgmManager = manager
             removeCalibrations()
             removeCalibrations()
-//            glucoseSource = nil
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             updateManagerUnits(cgmManager)
             updateManagerUnits(cgmManager)
@@ -234,40 +240,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return Manager.init(rawState: rawState)
         return Manager.init(rawState: rawState)
     }
     }
 
 
-    private func fetchGlucose() async throws -> [GlucoseStored]? {
-        try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.predicateFor30MinAgo,
-            key: "date",
-            ascending: false,
-            fetchLimit: 6
-        ) as? [GlucoseStored]
-    }
-
-    private func processGlucose() async throws -> [BloodGlucose] {
-        let results = try await fetchGlucose()
-
-        return try await context.perform {
-            guard let results else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return results.map { result in
-                BloodGlucose(
-                    sgv: Int(result.glucose),
-                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
-                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
-                    dateString: result.date ?? Date(),
-                    unfiltered: Decimal(result.glucose),
-                    filtered: Decimal(result.glucose),
-                    noise: nil,
-                    glucose: Int(result.glucose),
-                    type: "sgv"
-                )
-            }
-        }
-    }
-
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
         // calibration add if required only for sensor
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
         let newGlucose = overcalibrate(entries: glucose)
@@ -303,21 +275,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
         }
         debug(.deviceManager, "New glucose found")
         debug(.deviceManager, "New glucose found")
 
 
-        // filter the data if it is the case
-        if settingsManager.settings.smoothGlucose {
-            // limited to 30 min of old glucose data
-            let oldGlucoseValues = try await processGlucose()
+        try await glucoseStorage.storeGlucose(filtered)
 
 
-            var smoothedValues = oldGlucoseValues + filtered
-            // smooth with 3 repeats
-            for _ in 1 ... 3 {
-                smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
-            }
-            // find the new values only
-            filtered = smoothedValues.filter { $0.dateString > syncDate }
+        if settingsManager.settings.smoothGlucose {
+            await exponentialSmoothingGlucose(context: context)
         }
         }
 
 
-        try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
         deviceDataManager.heartbeat(date: Date())
 
 
         endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
         endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
@@ -377,3 +340,217 @@ extension CGMManager {
         ]
         ]
     }
     }
 }
 }
+
+extension BaseFetchGlucoseManager: SettingsObserver {
+    /// Smooth glucose data when smoothing is turned on.
+    func settingsDidChange(_: TrioSettings) {
+        let smoothingWasEnabled = shouldSmoothGlucose
+        let smoothingIsEnabled = settingsManager.settings.smoothGlucose
+        shouldSmoothGlucose = smoothingIsEnabled
+
+        guard smoothingIsEnabled, !smoothingWasEnabled else { return }
+
+        processQueue.async { [weak self] in
+            guard let self else { return }
+
+            self.glucoseStoreAndHeartLock.wait()
+            Task {
+                await self.exponentialSmoothingGlucose(context: self.context)
+                self.glucoseStoreAndHeartLock.signal()
+            }
+        }
+    }
+}
+
+extension BaseFetchGlucoseManager {
+    func fetchGlucose(context: NSManagedObjectContext) async throws -> [NSManagedObjectID] {
+        // Compound predicate: time window + non-manual + valid date
+        let timePredicate = NSPredicate.predicateForOneDayAgoInMinutes
+        let manualPredicate = NSPredicate(format: "isManual == NO")
+        let datePredicate = NSPredicate(format: "date != nil")
+
+        let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+            timePredicate,
+            manualPredicate,
+            datePredicate
+        ])
+
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            // Predicate must cover at least the full glucose horizon used by downstream algorithm consumers.
+            // If autosens / oref / smoothing logic ever starts looking back further (e.g. 36h),
+            // this fetch window must be expanded accordingly.
+            predicate: compoundPredicate,
+            key: "date",
+            ascending: true, // the first element is the oldest
+            fetchLimit: 350
+        )
+
+        guard let glucoseArray = results as? [GlucoseStored] else {
+            throw CoreDataError.fetchError(function: #function, file: #file)
+        }
+
+        return glucoseArray.map(\.objectID)
+    }
+
+    /// CoreData-friendly AAPS exponential smoothing + storage.
+    /// - Important: Only stores `smoothedGlucose`. UI/alerts should still use `glucose`.
+    ///
+    func exponentialSmoothingGlucose(context: NSManagedObjectContext) async {
+        let startTime = Date()
+
+        do {
+            // get objectIDs
+            let objectIDs = try await fetchGlucose(context: context)
+
+            try await context.perform {
+                // Load managed objects from object IDs
+                // Filtering (isManual, date) already done at DB level in fetchGlucose
+                let glucoseReadings = objectIDs.compactMap {
+                    context.object(with: $0) as? GlucoseStored
+                }
+
+                guard !glucoseReadings.isEmpty else { return }
+
+                // Static method call to avoid self-capture
+                Self.applyExponentialSmoothingAndStore(
+                    glucoseReadings: glucoseReadings,
+                    minimumWindowSize: 4,
+                    maximumAllowedGapMinutes: 12,
+                    xDripErrorGlucose: 38,
+                    minimumSmoothedGlucose: 39,
+                    firstOrderWeight: 0.4,
+                    firstOrderAlpha: 0.5,
+                    secondOrderAlpha: 0.4,
+                    secondOrderBeta: 1.0
+                )
+
+                try context.save()
+            }
+
+            let duration = Date().timeIntervalSince(startTime)
+            debugPrint(String(format: "Exponential smoothing duration: %0.04fs", duration))
+        } catch {
+            debug(.deviceManager, "Failed to smooth glucose: \(error)")
+        }
+    }
+
+    private static func applyExponentialSmoothingAndStore(
+        glucoseReadings data: [GlucoseStored],
+        minimumWindowSize: Int,
+        maximumAllowedGapMinutes: Int,
+        xDripErrorGlucose: Int,
+        minimumSmoothedGlucose: Decimal,
+        firstOrderWeight: Decimal,
+        firstOrderAlpha: Decimal,
+        secondOrderAlpha: Decimal,
+        secondOrderBeta: Decimal
+    ) {
+        guard !data.isEmpty else { return }
+
+        // Determine the size of the valid most-recent smoothing window.
+        // We walk adjacent pairs from newest -> oldest to preserve the same window semantics
+        // as the original implementation, but avoid manual reverse indexing.
+        var validWindowCount = max(data.count - 1, 0)
+
+        for (recentOffset, pair) in zip(data.dropFirst().reversed(), data.dropLast().reversed()).enumerated() {
+            let (newer, older) = pair
+
+            guard let newerDate = newer.date, let olderDate = older.date else { continue }
+
+            let gapSeconds = newerDate.timeIntervalSince(olderDate)
+            let gapMinutesRounded = Int((gapSeconds / 60.0).rounded())
+
+            if gapMinutesRounded >= maximumAllowedGapMinutes {
+                validWindowCount = recentOffset + 1 // include the more recent reading
+                break
+            }
+
+            // Ported from AAPS: 38 mg/dL may represent an xDrip error state.
+            if Int(newer.glucose) == xDripErrorGlucose {
+                validWindowCount = recentOffset // exclude this 38 value
+                break
+            }
+        }
+
+        // Not enough recent contiguous readings to smooth (e.g. after CGM gap).
+        // IMPORTANT: Only apply fallback to the recent window, not all data.
+        // Otherwise a recent gap would overwrite historical smoothed values.
+        guard validWindowCount >= minimumWindowSize else {
+            let recentWindow = data.suffix(validWindowCount)
+
+            for object in recentWindow {
+                let raw = Decimal(Int(object.glucose))
+                object.smoothedGlucose = max(raw, minimumSmoothedGlucose) as NSDecimalNumber
+            }
+
+            return
+        }
+
+        // Restrict smoothing to the valid most-recent window, still in chronological order.
+        let validWindow = data.suffix(validWindowCount)
+
+        guard let oldest = validWindow.first else { return }
+
+        // ---- 1st order smoothing ----
+        var firstOrderSmoothed: [Decimal] = []
+        firstOrderSmoothed.reserveCapacity(validWindow.count)
+
+        var firstOrderCurrent = Decimal(Int(oldest.glucose))
+        firstOrderSmoothed.append(firstOrderCurrent)
+
+        for sample in validWindow.dropFirst() {
+            let raw = Decimal(Int(sample.glucose))
+            firstOrderCurrent = firstOrderCurrent + firstOrderAlpha * (raw - firstOrderCurrent)
+            firstOrderSmoothed.append(firstOrderCurrent)
+        }
+
+        // ---- 2nd order smoothing ----
+        let secondOrderInput = Array(validWindow)
+        guard secondOrderInput.count >= 2 else { return }
+
+        var secondOrderSmoothed: [Decimal] = []
+        secondOrderSmoothed.reserveCapacity(secondOrderInput.count)
+
+        var secondOrderDeltas: [Decimal] = []
+        secondOrderDeltas.reserveCapacity(secondOrderInput.count)
+
+        var previousSecondOrderSmoothed = Decimal(Int(secondOrderInput[0].glucose))
+        var previousSecondOrderDelta =
+            Decimal(Int(secondOrderInput[1].glucose) - Int(secondOrderInput[0].glucose))
+
+        secondOrderSmoothed.append(previousSecondOrderSmoothed)
+        secondOrderDeltas.append(previousSecondOrderDelta)
+
+        for sample in secondOrderInput.dropFirst() {
+            let raw = Decimal(Int(sample.glucose))
+
+            let nextSmoothed =
+                secondOrderAlpha * raw
+                    + (1 - secondOrderAlpha) * (previousSecondOrderSmoothed + previousSecondOrderDelta)
+
+            let nextDelta =
+                secondOrderBeta * (nextSmoothed - previousSecondOrderSmoothed)
+                    + (1 - secondOrderBeta) * previousSecondOrderDelta
+
+            previousSecondOrderSmoothed = nextSmoothed
+            previousSecondOrderDelta = nextDelta
+
+            secondOrderSmoothed.append(nextSmoothed)
+            secondOrderDeltas.append(nextDelta)
+        }
+
+        // ---- Weighted blend ----
+        let blended = zip(firstOrderSmoothed, secondOrderSmoothed).map { firstOrder, secondOrder in
+            firstOrderWeight * firstOrder + (1 - firstOrderWeight) * secondOrder
+        }
+
+        // Apply to the most recent valid-window readings.
+        for (object, blendedValue) in zip(validWindow, blended) {
+            let rounded = blendedValue.rounded(toPlaces: 0) // nearest integer, ties away from zero
+            let clamped = max(rounded, minimumSmoothedGlucose)
+            object.smoothedGlucose = clamped as NSDecimalNumber
+        }
+    }
+}

+ 108 - 12
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -98,7 +98,12 @@ final class OpenAPS {
     }
     }
 
 
     // fetch glucose to pass it to the meal function and to determine basal
     // fetch glucose to pass it to the meal function and to determine basal
-    private func fetchAndProcessGlucose(fetchLimit: Int?) async throws -> String {
+    func fetchAndProcessGlucose(
+        context: NSManagedObjectContext,
+        shouldSmoothGlucose: Bool,
+        fetchLimit: Int?
+    ) async throws -> String {
+        // make it async and await it
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
@@ -109,14 +114,40 @@ final class OpenAPS {
             batchSize: 48
             batchSize: 48
         )
         )
 
 
-        return try await context.perform {
+        // mapping within the context closure, JSON conversion outside
+        let algorithmGlucose = try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
-            // convert to JSON
-            return self.jsonConverter.convertToJSON(glucoseResults)
+            // extracting handler to only create it 1x
+            let roundingBehavior = NSDecimalNumberHandler(
+                roundingMode: .plain,
+                scale: 0,
+                raiseOnExactness: false,
+                raiseOnOverflow: false,
+                raiseOnUnderflow: false,
+                raiseOnDivideByZero: false
+            )
+
+            return glucoseResults.map { glucose -> AlgorithmGlucose in
+                let glucoseValue: Int16
+                if shouldSmoothGlucose, !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0 {
+                    glucoseValue = smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value
+                } else {
+                    glucoseValue = glucose.glucose
+                }
+                return AlgorithmGlucose(
+                    date: glucose.date,
+                    direction: glucose.direction,
+                    glucose: glucoseValue,
+                    id: glucose.id,
+                    isManual: glucose.isManual
+                )
+            }
         }
         }
+
+        return jsonConverter.convertToJSON(algorithmGlucose)
     }
     }
 
 
     private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
     private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
@@ -193,14 +224,22 @@ final class OpenAPS {
     private func parsePumpHistory(
     private func parsePumpHistory(
         _ pumpHistoryObjectIDs: [NSManagedObjectID],
         _ pumpHistoryObjectIDs: [NSManagedObjectID],
         simulatedBolusAmount: Decimal? = nil
         simulatedBolusAmount: Decimal? = nil
-    ) async -> String {
+    ) async throws -> String {
         // Return an empty JSON object if the list of object IDs is empty
         // Return an empty JSON object if the list of object IDs is empty
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
 
 
+        // Addresses https://github.com/nightscout/Trio/issues/898
+        //
+        // On a cold start (new user, fresh onboarding, or pump disconnected > 24h),
+        // the oldest event in pump history can be a resume with no preceding pump
+        // activity. oref interprets this as the end of a suspend that never started,
+        // which drives negative IOB and can cause excessive insulin delivery.
+        let orphanedResumes = try await fetchOrphanedResumes()
+
         // Execute all operations on the background context
         // Execute all operations on the background context
         return await context.perform {
         return await context.perform {
             // Load and map pump events to DTOs
             // Load and map pump events to DTOs
-            var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
+            var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes)
 
 
             // Optionally add the IOB as a DTO
             // Optionally add the IOB as a DTO
             if let simulatedBolusAmount = simulatedBolusAmount {
             if let simulatedBolusAmount = simulatedBolusAmount {
@@ -213,17 +252,23 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    private func loadAndMapPumpEvents(_ pumpHistoryObjectIDs: [NSManagedObjectID]) -> [PumpEventDTO] {
-        OpenAPS.loadAndMapPumpEvents(pumpHistoryObjectIDs, from: context)
+    private func loadAndMapPumpEvents(
+        _ pumpHistoryObjectIDs: [NSManagedObjectID],
+        orphanedResumes: [NSManagedObjectID]
+    ) -> [PumpEventDTO] {
+        OpenAPS.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes, from: context)
     }
     }
 
 
     /// Fetches and parses pump events, expose this as static and not private for testing
     /// Fetches and parses pump events, expose this as static and not private for testing
     static func loadAndMapPumpEvents(
     static func loadAndMapPumpEvents(
         _ pumpHistoryObjectIDs: [NSManagedObjectID],
         _ pumpHistoryObjectIDs: [NSManagedObjectID],
+        orphanedResumes: [NSManagedObjectID],
         from context: NSManagedObjectContext
         from context: NSManagedObjectContext
     ) -> [PumpEventDTO] {
     ) -> [PumpEventDTO] {
+        let orphanedSet = Set(orphanedResumes)
+        let filteredObjectIds = pumpHistoryObjectIDs.filter { !orphanedSet.contains($0) }
         // Load the pump events from the object IDs
         // Load the pump events from the object IDs
-        let pumpHistory: [PumpEventStored] = pumpHistoryObjectIDs
+        let pumpHistory: [PumpEventStored] = filteredObjectIds
             .compactMap { context.object(with: $0) as? PumpEventStored }
             .compactMap { context.object(with: $0) as? PumpEventStored }
 
 
         // Create the DTOs
         // Create the DTOs
@@ -276,8 +321,59 @@ final class OpenAPS {
         return .bolus(bolusDTO)
         return .bolus(bolusDTO)
     }
     }
 
 
+    /// Detects a cold-start orphaned resume: returns the resume's object ID if it's an orphaned resume
+    private func fetchOrphanedResumes() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpHistoryLast48h,
+            key: "timestamp",
+            ascending: true,
+            batchSize: 250
+        )
+
+        return try await context.perform {
+            guard let pumpEventResultsFull = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
+
+            let pumpEventResults = pumpEventResultsFull
+                .filter { $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue }
+
+            // we define an orphaned resume as one without a paired suspend within
+            // the most recent 24 hours.
+            // **Important**: we pick 48 hours because the standard pump history
+            // is 24 hours + 24 hours of inspection for resumes.
+            let orphanedResumes = zip(pumpEventResults, pumpEventResults.dropFirst())
+                .compactMap { (prev, curr) -> PumpEventStored? in
+                    guard let prevTimestamp = prev.timestamp, let currTimestamp = curr.timestamp else {
+                        return nil
+                    }
+                    let interval = currTimestamp.timeIntervalSince(prevTimestamp)
+
+                    // check if the current event is an orphaned resume
+                    //  - previous event not a suspend
+                    //  - previous event is a suspend but it's more than 24 hours ago
+                    if curr.type == EventType.pumpResume.rawValue,
+                       prev.type != EventType.pumpSuspend.rawValue || interval > TimeInterval(hours: 24)
+                    {
+                        return curr
+                    }
+                    return nil
+                }
+            // check the first event to see if it's an orphaned resume
+            let firstResumeOrphaned = pumpEventResults.first.flatMap({ event -> [PumpEventStored]? in
+                guard event.type == EventType.pumpResume.rawValue else { return nil }
+                return [event]
+            }) ?? []
+
+            return (firstResumeOrphaned + orphanedResumes).map(\.objectID)
+        }
+    }
+
     func determineBasal(
     func determineBasal(
         currentTemp: TempBasal,
         currentTemp: TempBasal,
+        shouldSmoothGlucose: Bool,
         clock: Date = Date(),
         clock: Date = Date(),
         simulatedCarbsAmount: Decimal? = nil,
         simulatedCarbsAmount: Decimal? = nil,
         simulatedBolusAmount: Decimal? = nil,
         simulatedBolusAmount: Decimal? = nil,
@@ -292,7 +388,7 @@ final class OpenAPS {
         // Perform asynchronous calls in parallel
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
         async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
-        async let glucose = fetchAndProcessGlucose(fetchLimit: 72)
+        async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: 72)
         async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
         async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
@@ -461,13 +557,13 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    func autosense() async throws -> Autosens? {
+    func autosense(shouldSmoothGlucose: Bool) async throws -> Autosens? {
         debug(.openAPS, "Start autosens")
         debug(.openAPS, "Start autosens")
 
 
         // Perform asynchronous calls in parallel
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let carbs = fetchAndProcessCarbs()
         async let carbs = fetchAndProcessCarbs()
-        async let glucose = fetchAndProcessGlucose(fetchLimit: nil)
+        async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: nil)
         async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
         async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
         async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)
         async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)

+ 114 - 59
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -102,38 +102,40 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
     }
 
 
     /**
     /**
-     Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap.
+     Converts fat and protein into delayed carb-equivalent entries (FPU handling).
 
 
-     - The function uses predefined rules to determine the duration based on the number of FPUs.
-     - Ensures that the duration does not exceed the time cap.
+     Behavior:
 
 
-     - Parameters:
-       - fpus: The number of FPUs calculated from fat and protein.
-       - timeCap: The maximum allowed duration.
+     - Calculates carb equivalents from fat and protein
+       ((fat × 9 + protein × 4) / 10 × adjustment factor).
+     - Rounds down to whole grams.
+     - Drops values below 10 g.
+     - Caps total equivalents at 99 g.
+     - Splits into up to 3 entries.
+     - Caps each entry at 33 g.
+     - Distributes grams as evenly as possible.
 
 
-     - Returns: The computed duration in hours.
-     */
-    private func calculateComputedDuration(fpus: Decimal, timeCap: Decimal) -> Decimal {
-        switch fpus {
-        case ..<2:
-            return 3
-        case 2 ..< 3:
-            return 4
-        case 3 ..< 4:
-            return 5
-        default:
-            return timeCap
-        }
-    }
+     Timing:
 
 
-    /**
-     Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams.
+     - First entry is scheduled after the configured delay
+       (default: 60 minutes) from the carb entry timestamp.
+     - Additional entries are spaced 30 minutes apart.
+
+     Example (default):
+
+     - Carb entry at T
+     - 1st equivalent at T + 60 min
+     - 2nd equivalent at T + 90 min
+     - 3rd equivalent at T + 120 min
 
 
-     - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams.
-     - Creates future carb entries based on the adjusted carb equivalent size and interval.
+     Generated entries:
+
+     - Are marked with `isFPU = true`
+     - Contain only carbs (fat and protein set to 0)
+     - Share the same `fpuID` as the original carb entry
 
 
      - Parameters:
      - Parameters:
-       - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed.
+       - entries: An array of `CarbsEntry` objects representing the carb equivalent entries to be processed.
        - fat: The amount of fat in the last entry.
        - fat: The amount of fat in the last entry.
        - protein: The amount of protein in the last entry.
        - protein: The amount of protein in the last entry.
        - createdAt: The creation date of the last entry.
        - createdAt: The creation date of the last entry.
@@ -150,46 +152,48 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         let trioSettings = settings.settings
         let trioSettings = settings.settings
         let providerSettings = settingsProvider.settings
         let providerSettings = settingsProvider.settings
 
 
-        let interval = trioSettings.minuteInterval.clamp(to: providerSettings.minuteInterval)
-        let timeCap = trioSettings.timeCap.clamp(to: providerSettings.timeCap)
-        let adjustment = trioSettings.individualAdjustmentFactor.clamp(to: providerSettings.individualAdjustmentFactor)
-        let delay = trioSettings.delay.clamp(to: providerSettings.delay)
+        let adjustment = trioSettings.individualAdjustmentFactor
+            .clamp(to: providerSettings.individualAdjustmentFactor)
 
 
-        let kcal = protein * 4 + fat * 9
-        let carbEquivalents = (kcal / 10) * adjustment
-        let fpus = carbEquivalents / 10
-        var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
+        let delayMinutes = trioSettings.delay
+            .clamp(to: providerSettings.delay)
 
 
-        var carbEquivalentSize: Decimal = carbEquivalents / computedDuration
-        carbEquivalentSize /= Decimal(60) / interval
+        let spreadInterval = trioSettings.minuteInterval
+            .clamp(to: providerSettings.minuteInterval)
 
 
-        if carbEquivalentSize < 1.0 {
-            carbEquivalentSize = 1.0
-            computedDuration = min(carbEquivalents / carbEquivalentSize, timeCap)
+        // Constraints
+        let maxTotalGrams = 99
+        let maxEntries = 3
+        let maxPerEntry = 33
+        let minPerEntry = 10
+        let spacing = TimeInterval(spreadInterval * 60)
+
+        // kcal -> carb equivalents (kcal/10 * adjustment), rounded down to whole grams
+        let kcal = protein * 4 + fat * 9
+        let rawEquivalents = Int((kcal / 10) * adjustment)
+        let totalGrams = min(maxTotalGrams, max(0, rawEquivalents))
+
+        guard totalGrams >= minPerEntry else {
+            return ([], Decimal(totalGrams))
         }
         }
 
 
-        let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
-        carbEquivalentSize = Decimal(roundedEquivalent)
-        var numberOfEquivalents = carbEquivalents / carbEquivalentSize
+        let amounts = splitIntoCarbEquivalents(
+            total: totalGrams,
+            maxEntries: maxEntries,
+            maxPerEntry: maxPerEntry,
+            minPerEntry: minPerEntry
+        )
 
 
-        var useDate = actualDate ?? createdAt
+        let baseDate = actualDate ?? createdAt
+        let start = baseDate.addingTimeInterval(TimeInterval(delayMinutes * 60))
         let fpuID = entries.first?.fpuID ?? UUID().uuidString
         let fpuID = entries.first?.fpuID ?? UUID().uuidString
-        var futureCarbArray = [CarbsEntry]()
-        var firstIndex = true
-
-        // convert Decimal minutes to TimeInterval in seconds
-        let delayTimeInterval = TimeInterval(delay * 60)
-        let intervalTimeInterval = TimeInterval(interval * 60)
-        while carbEquivalents > 0, numberOfEquivalents > 0 {
-            useDate = firstIndex ? useDate.addingTimeInterval(delayTimeInterval) : useDate
-                .addingTimeInterval(intervalTimeInterval)
-            firstIndex = false
-
-            let eachCarbEntry = CarbsEntry(
+
+        let futureEntries: [CarbsEntry] = amounts.enumerated().map { idx, grams in
+            CarbsEntry(
                 id: UUID().uuidString,
                 id: UUID().uuidString,
                 createdAt: createdAt,
                 createdAt: createdAt,
-                actualDate: useDate,
-                carbs: carbEquivalentSize,
+                actualDate: start.addingTimeInterval(TimeInterval(idx) * spacing),
+                carbs: Decimal(grams),
                 fat: 0,
                 fat: 0,
                 protein: 0,
                 protein: 0,
                 note: nil,
                 note: nil,
@@ -197,11 +201,62 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 isFPU: true,
                 isFPU: true,
                 fpuID: fpuID
                 fpuID: fpuID
             )
             )
-            futureCarbArray.append(eachCarbEntry)
-            numberOfEquivalents -= 1
         }
         }
 
 
-        return (futureCarbArray, carbEquivalents)
+        let totalScheduled = futureEntries.reduce(into: Decimal(0)) { $0 += $1.carbs }
+        return (futureEntries, totalScheduled)
+    }
+
+    /**
+     Splits a total carb-equivalent value into multiple integer entries.
+
+     - Returns no entries if `total` is below `minPerEntry`.
+     - Limits output to `maxEntries`.
+     - Caps each entry at `maxPerEntry`.
+     - Distributes grams evenly (difference ≤ 1 g).
+     - Merges or removes entries below `minPerEntry`.
+
+     - Returns:
+       Integer gram values representing the split carb equivalents.
+     */
+    private func splitIntoCarbEquivalents(
+        total: Int,
+        maxEntries: Int,
+        maxPerEntry: Int,
+        minPerEntry: Int
+    ) -> [Int] {
+        guard total >= minPerEntry else { return [] }
+
+        // Choose an entry count that *guarantees* each entry can be <= maxPerEntry
+        let needed = (total + maxPerEntry - 1) / maxPerEntry
+        let count = min(maxEntries, max(1, needed))
+
+        // Even split (difference between buckets is at most 1)
+        func evenSplit(_ total: Int, count: Int) -> [Int] {
+            let base = total / count
+            let rem = total % count
+            return (0 ..< count).map { base + ($0 < rem ? 1 : 0) }
+        }
+
+        var buckets = evenSplit(total, count: count)
+
+        // Enforce minPerEntry by merging any too-small tail bucket into the previous one
+        // This should be rare, but it keeps the invariant
+        if buckets.count > 1 {
+            for i in stride(from: buckets.count - 1, through: 1, by: -1) {
+                let v = buckets[i]
+                guard v > 0, v < minPerEntry else { continue }
+                buckets[i - 1] += v
+                buckets[i] = 0
+            }
+            buckets = buckets.filter { $0 > 0 }
+        }
+
+        // Guarantee not to exceed maxPerEntry if merging a reduced count
+        // Clamp as final guard here
+        buckets = buckets.map { min(maxPerEntry, $0) }.filter { $0 >= minPerEntry }
+
+        return buckets
     }
     }
 
 
     private func saveCarbEquivalents(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
     private func saveCarbEquivalents(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {

+ 6 - 3
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -291,13 +291,16 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     }
     }
 
 
     func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
     func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
-        if event.bolus!.isSMB {
+        guard let bolus = event.bolus else {
+            return event.type.flatMap({ PumpEventStored.EventType(rawValue: $0) }) ?? .bolus
+        }
+        if bolus.isSMB {
             return .smb
             return .smb
         }
         }
-        if event.bolus!.isExternal {
+        if bolus.isExternal {
             return .isExternal
             return .isExternal
         }
         }
-        return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
+        return event.type.flatMap({ PumpEventStored.EventType(rawValue: $0) }) ?? .bolus
     }
     }
 
 
     func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
     func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {

+ 5 - 0
Trio/Sources/Helpers/Decimal+Extensions.swift

@@ -5,6 +5,11 @@ extension Double {
     init(_ decimal: Decimal) {
     init(_ decimal: Decimal) {
         self.init(truncating: decimal as NSNumber)
         self.init(truncating: decimal as NSNumber)
     }
     }
+
+    func roundedDouble(toPlaces places: Int) -> Double {
+        let divisor = pow(10.0, Double(places))
+        return (self * divisor).rounded() / divisor
+    }
 }
 }
 
 
 extension Int {
 extension Int {

+ 7 - 0
Trio/Sources/Helpers/Formatters.swift

@@ -57,6 +57,13 @@ extension Formatter {
         return formatter
         return formatter
     }()
     }()
 
 
+    static let timeForLogFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH:mm:ss"
+        formatter.timeZone = TimeZone.current
+        return formatter
+    }()
+
     static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
     static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.numberStyle = .decimal

+ 0 - 172
Trio/Sources/Helpers/SavitzkyGolayFilter.swift

@@ -1,172 +0,0 @@
-import Foundation
-
-/// allowed values are 0, 1, 2 or 3. It's the index in coefficients
-private var coefficientsRowToUse = 3
-
-/// Savitzky Golay coefficients
-private let coefficients = [
-    [-3.0, 12.0, 17.0, 12.0, -3.0],
-    [-2.0, 3.0, 6.0, 7.0, 6.0, 3.0, -2.0],
-    [-21.0, 14.0, 39.0, 54.0, 59.0, 54.0, 39.0, 14.0, -21.0],
-    [-36.0, 9.0, 44.0, 69.0, 84.0, 89.0, 84.0, 69.0, 44.0, 9.0, -36.0]
-]
-
-/// an array with elements of a type that conforms to Smoothable, can be filtered using  the Savitzky Golay algorithm
-protocol SavitzkyGolaySmoothable {
-    /// value to be smoothed
-    var value: Double { get set }
-}
-
-/// local help class
-private class IsSmoothable: SavitzkyGolaySmoothable {
-    var value: Double = 0.0
-
-    init(withValue value: Double = 0.0) {
-        self.value = value
-    }
-}
-
-extension Array where Element: SavitzkyGolaySmoothable {
-    /// - apply Savitzky Golay filter
-    /// - before applying the filter, the array will be prepended and append with a number of elements equal to the filterwidth, filterWidth default 5. Allowed values are 5, 4, 3, 2. If any other value is assigned, then 5 will be used
-    /// - ...continue with 5 here in the explanation ...
-    /// - for the 5 last elements and 5 first elements, a regression is done. This regression is done used to give values to the 5 prepended and appended values. Which means it's as if we draw a line through the first 5 and 5 last original values, and use this line to give values to the 5 prepended and appended values
-    /// - the 5 prepended and appended values are then used in the filter algorithm, which means we can also filter the original 5 first and last elements
-    /// see also example https://github.com/JohanDegraeve/xdripswift/wiki/Libre-value-smoothing
-    mutating func smoothSavitzkyGolayQuaDratic(withFilterWidth filterWidth: Int = 5) {
-        // filterWidthToUse is the value of filterWidth to use in the algorithm. By default filterWidthToUse = parameter value filterWidth
-        var filterWidthToUse = filterWidth
-
-        // calculate coefficientsRowToUse based on filterWdith
-        switch filterWidth {
-        case 5:
-            coefficientsRowToUse = 3
-
-        case 4:
-            coefficientsRowToUse = 2
-
-        case 3:
-            coefficientsRowToUse = 1
-
-        case 2:
-            coefficientsRowToUse = 0
-
-        default:
-            // invalid filterWidth was given in parameterList, use default value
-            coefficientsRowToUse = 3
-
-            filterWidthToUse = 5
-        }
-
-        // using 5 here in the comments as value for filterWidthToUse
-
-        // the amount of elements must be at least 5. If that's not the case then don't apply any smoothing
-        guard count >= filterWidthToUse else { return }
-
-        // create a new array, to which we will prepend and append 5 elements so that we can do also smoothing for the 5 last and 5 first values of the input array (which is self)
-        // the 5 elements will be estimated by doing linear regression of the first 5 and last 5 elements of the original input array respectively
-        // this is only a temporary array, but it will hold the elements of the original array, those elements will get a new value when doing the smoothing
-        var tempArray = [SavitzkyGolaySmoothable]()
-        for element in self {
-            tempArray.append(element)
-        }
-
-        // now prepend and append with 5 elements, each with a default value 0.0
-        for _ in 0 ..< filterWidthToUse {
-            tempArray.insert(IsSmoothable(), at: 0)
-            tempArray.append(IsSmoothable())
-        }
-
-        // so now we have tempArray, of length size of original array + 2 * 5
-        // the first 5 and the last 5 elements are of type IsSmoothable with value 0
-
-        // - indicesArray is a help array needed for the function linearRegressionCreator
-        // - this will be the first parameter in the call to the linearRegression function, in fact it's an array of IsSmoothable with length = length of tempArray
-        // - we give each IsSmoothable the value of the index, meaning from 0 up to (length of tempArray) - 1
-        // - in fact it's not really smoothable, it's just because we use isSmoothable in function linearRegressionCreator
-        var indicesArray = [SavitzkyGolaySmoothable]()
-        for index in 0 ..< (count + (filterWidthToUse * 2)) {
-            indicesArray.append(IsSmoothable(withValue: Double(index)))
-        }
-
-        /// - this is a piece of code that we will execute two times, once for the firs 5 elements, then for the last 5, so we put it in a closure variable
-        /// - it calculates the regression function (which is nothing else but doing y = intercept + slope*x) for range defined by predictorRange in tempArray. It will be used for the 5 first and 5 last real values, ie the 5 first and 5 last real glucose values
-        /// - then executes the regression for every element in the range defined by targetRange, again in tempArray
-        let doRegression = { (predictorRange: Range<Int>, targetRange: Range<Int>) in
-
-            // calculate the linearRegression function
-            let linearRegression = linearRegressionCreator(indicesArray[predictorRange], tempArray[predictorRange])
-
-            // ready to do the linear regression for the targetRange in tempArray
-            for index in targetRange {
-                tempArray[index].value = linearRegression(indicesArray[index].value)
-            }
-        }
-
-        // now do the regression for the 5 first elements
-        doRegression(filterWidthToUse ..< (filterWidthToUse * 2), 0 ..< filterWidthToUse)
-
-        // now do the regression for the 5 last elements
-        doRegression(
-            (tempArray.count - filterWidthToUse * 2) ..< (tempArray.count - filterWidthToUse),
-            (tempArray.count - filterWidthToUse) ..< tempArray.count
-        )
-
-        // now start filtering
-
-        // initialize array that will hold the resulting filtered values
-        var filteredValues = [Double]()
-
-        // calculate divider
-        let divider = coefficients[coefficientsRowToUse].reduce(0, { x, y in
-            x + y
-        })
-
-        // filter each original value
-        for _ in 0 ..< count {
-            // add a new element to filteredValues, start value is 0.0
-            // this new value will be the last element, so we access it with index filteredValues.count - 1
-            filteredValues.append(0.0)
-
-            // iterate through the coefficients
-            for (index, coefficient) in coefficients[coefficientsRowToUse].enumerated() {
-                filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] + coefficient *
-                    tempArray[index + filteredValues.count - 1].value
-            }
-
-            filteredValues[filteredValues.count - 1] = filteredValues[filteredValues.count - 1] / divider
-        }
-
-        // now assign the new values to the original objects
-        for (index, _) in enumerated() {
-            self[index].value = filteredValues[index]
-        }
-    }
-}
-
-/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
-private func multiply(
-    _ a: ArraySlice<SavitzkyGolaySmoothable>,
-    _ b: ArraySlice<SavitzkyGolaySmoothable>
-) -> ArraySlice<SavitzkyGolaySmoothable> {
-    zip(a, b).map({ IsSmoothable(withValue: $0.value * $1.value) })[0 ..< a.count]
-}
-
-/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
-private func average(_ input: ArraySlice<SavitzkyGolaySmoothable>) -> Double {
-    (input.reduce(IsSmoothable(), { (x: SavitzkyGolaySmoothable, y: SavitzkyGolaySmoothable) in
-        IsSmoothable(withValue: x.value + y.value) })).value / Double(input.count)
-}
-
-/// source https://github.com/raywenderlich/swift-algorithm-club/tree/master/Linear%20Regression
-private func linearRegressionCreator(
-    _ xs: ArraySlice<SavitzkyGolaySmoothable>,
-    _ ys: ArraySlice<SavitzkyGolaySmoothable>
-) -> (Double) -> Double {
-    let sum1 = average(multiply(ys, xs)) - average(xs) * average(ys)
-    let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
-    let slope = sum1 / sum2
-    let intercept = average(ys) - slope * average(xs)
-
-    return { x in intercept + slope * x }
-}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 7472 - 1217
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 86 - 0
Trio/Sources/Models/AlgorithmGlucose.swift

@@ -0,0 +1,86 @@
+import Foundation
+
+/// Helper class so that we can have a plain Swift object to serialize GlucoseStorage
+struct AlgorithmGlucose: Codable {
+    var date: Date?
+    var direction: String?
+    var glucose: Int16
+    var id: UUID?
+    var isManual: Bool
+
+    enum CodingKeys: String, CodingKey {
+        case date
+        case dateString
+        case sgv
+        case glucose
+        case direction
+        case id
+        case type
+    }
+
+    init(date: Date?, direction: String?, glucose: Int16, id: UUID?, isManual: Bool) {
+        self.date = date
+        self.direction = direction
+        self.glucose = glucose
+        self.id = id
+        self.isManual = isManual
+    }
+
+    // this constructor is just for testing
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        if let dateString = try container.decodeIfPresent(String.self, forKey: .dateString) {
+            let dateFormatter = ISO8601DateFormatter()
+            dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+            date = dateFormatter.date(from: dateString)
+        } else if let dateStringTimestamp = try container.decodeIfPresent(String.self, forKey: .date),
+                  let dateTimestamp = TimeInterval(dateStringTimestamp)
+        {
+            date = Date(timeIntervalSince1970: dateTimestamp / 1000)
+        } else {
+            date = nil
+        }
+
+        direction = try container.decodeIfPresent(String.self, forKey: .direction)
+        id = try container.decodeIfPresent(UUID.self, forKey: .id)
+
+        if let glucoseValue = try container.decodeIfPresent(Int16.self, forKey: .glucose) {
+            glucose = glucoseValue
+            isManual = true
+        } else if let sgvValue = try container.decodeIfPresent(Int16.self, forKey: .sgv) {
+            glucose = sgvValue
+            isManual = false
+        } else {
+            throw DecodingError.dataCorruptedError(
+                forKey: .sgv,
+                in: container,
+                debugDescription: "Neither 'glucose' nor 'sgv' key found or value is not Int16"
+            )
+        }
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+
+        let dateFormatter = ISO8601DateFormatter()
+        dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+
+        try container.encode(dateFormatter.string(from: date ?? Date()), forKey: .dateString)
+
+        let dateAsUnixTimestamp = String(format: "%.0f", (date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
+        try container.encode(dateAsUnixTimestamp, forKey: .date)
+
+        try container.encode(direction, forKey: .direction)
+        try container.encode(id, forKey: .id)
+
+        // TODO: Handle the type of the glucose entry conditionally not hardcoded
+        try container.encode("sgv", forKey: .type)
+
+        if isManual {
+            try container.encode(glucose, forKey: .glucose)
+        } else {
+            try container.encode(glucose, forKey: .sgv)
+        }
+    }
+}

+ 0 - 17
Trio/Sources/Models/Autotune.swift

@@ -1,17 +0,0 @@
-import Foundation
-
-struct Autotune: JSON, Equatable {
-    var createdAt: Date?
-    let basalProfile: [BasalProfileEntry]
-    let sensitivity: Decimal
-    let carbRatio: Decimal
-}
-
-extension Autotune {
-    private enum CodingKeys: String, CodingKey {
-        case createdAt = "created_at"
-        case basalProfile = "basalprofile"
-        case sensitivity = "sens"
-        case carbRatio = "carb_ratio"
-    }
-}

+ 0 - 12
Trio/Sources/Models/BloodGlucose.swift

@@ -249,18 +249,6 @@ extension NumberFormatter {
     }()
     }()
 }
 }
 
 
-extension BloodGlucose: SavitzkyGolaySmoothable {
-    var value: Double {
-        get {
-            Double(glucose ?? 0)
-        }
-        set {
-            glucose = Int(newValue)
-            sgv = Int(newValue)
-        }
-    }
-}
-
 extension BloodGlucose {
 extension BloodGlucose {
     func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
     func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
         StoredGlucoseSample(
         StoredGlucoseSample(

+ 22 - 0
Trio/Sources/Models/BolusDisplayThreshold.swift

@@ -0,0 +1,22 @@
+import Foundation
+
+enum BolusDisplayThreshold: Decimal, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    public var id: Decimal { rawValue }
+    case oneUnit = 1
+    case halfUnit = 0.5
+    case pointOneUnit = 0.1
+    case allUnits = 0.01
+
+    var displayName: String {
+        switch self {
+        case .oneUnit:
+            return String(localized: "1 U and over")
+        case .halfUnit:
+            return String(localized: "0.5 U and over")
+        case .pointOneUnit:
+            return String(localized: "0.1 U and over")
+        case .allUnits:
+            return String(localized: "Show All")
+        }
+    }
+}

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

@@ -132,8 +132,7 @@ struct DecimalPickerSettings {
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
     var delay = PickerSetting(value: 60, step: 5, min: 15, max: 120, type: PickerSetting.PickerSettingType.minute)
     var delay = PickerSetting(value: 60, step: 5, min: 15, max: 120, type: PickerSetting.PickerSettingType.minute)
-    var minuteInterval = PickerSetting(value: 30, step: 5, min: 10, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
+    var minuteInterval = PickerSetting(value: 30, step: 5, min: 30, max: 60, type: PickerSetting.PickerSettingType.minute)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, 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: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
     var maxBolus = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)

+ 0 - 24
Trio/Sources/Models/FetchedProfile.swift

@@ -1,24 +0,0 @@
-import Foundation
-
-struct FetchedNightscoutProfileStore: JSON {
-    let _id: String
-    let defaultProfile: String
-    let startDate: String
-    let mills: Decimal
-    let enteredBy: String
-    let store: [String: ScheduledNightscoutProfile]
-    let created_at: String
-}
-
-struct FetchedNightscoutProfile: JSON {
-    let dia: Decimal
-    let carbs_hr: Int
-    let delay: Decimal
-    let timezone: String
-    let target_low: [NightscoutTimevalue]
-    let target_high: [NightscoutTimevalue]
-    let sens: [NightscoutTimevalue]
-    let basal: [NightscoutTimevalue]
-    let carbratio: [NightscoutTimevalue]
-    let units: String
-}

+ 124 - 0
Trio/Sources/Models/GarminWatchSettings.swift

@@ -0,0 +1,124 @@
+import Foundation
+
+// MARK: - Garmin Data Type Settings
+
+/// Primary attribute selection for Garmin watchface and datafield.
+/// Determines whether to display COB, ISF, or Sensitivity Ratio alongside glucose data.
+/// Used by both Trio and SwissAlpine watchfaces.
+enum GarminPrimaryAttributeChoice: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+
+    case cob
+    case isf
+    case sensRatio
+
+    var displayName: String {
+        switch self {
+        case .cob:
+            return String(localized: "COB", comment: "")
+        case .isf:
+            return String(localized: "ISF", comment: "")
+        case .sensRatio:
+            return String(localized: "Sens Ratio", comment: "")
+        }
+    }
+}
+
+/// Secondary attribute selection for both Trio and SwissAlpine watchfaces.
+/// Determines whether to display Temp Basal Rate or Eventual BG.
+enum GarminSecondaryAttributeChoice: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+
+    case tbr
+    case eventualBG
+
+    var displayName: String {
+        switch self {
+        case .tbr:
+            return String(localized: "TBR", comment: "")
+        case .eventualBG:
+            return String(localized: "evBG", comment: "")
+        }
+    }
+}
+
+// MARK: - Garmin Watchface Setting
+
+/// Defines the available Garmin watchfaces with their associated UUIDs.
+enum GarminWatchface: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+
+    case trio
+    case swissalpine
+
+    var displayName: String {
+        switch self {
+        case .trio:
+            return String(localized: "Trio", comment: "")
+        case .swissalpine:
+            return String(localized: "Swissalpine", comment: "")
+        }
+    }
+
+    /// The UUID for the watchface application in Garmin Connect IQ
+    var watchfaceUUID: UUID? {
+        switch self {
+        case .trio:
+            // return UUID(uuidString: "EC3420F6-027D-49B3-B45F-D81D6D3ED90A")  // local build
+            // return UUID(uuidString: "81204522-B1BE-4E19-8E6E-C4032AAF8C6D") // ConnectIQ test build
+            return UUID(uuidString: "7a121867-140e-41ba-9982-2e82e2aa6579") // ConnectIQ live build
+        case .swissalpine:
+            // return UUID(uuidString: "5A643C13-D5A7-40D4-B809-84789FDF4A1F") // ConnectIQ test build
+            return UUID(uuidString: "4cea4efd-4aaf-4db4-8891-ef36dde14303") // ConnectIQ live build
+        }
+    }
+}
+
+// MARK: - Garmin Datafield Setting
+
+/// Defines the available Garmin datafields with their associated UUIDs.
+enum GarminDatafield: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+
+    case trio
+    case swissalpine
+    case none
+
+    var displayName: String {
+        switch self {
+        case .trio:
+            return String(localized: "Trio", comment: "")
+        case .swissalpine:
+            return String(localized: "Swissalpine", comment: "")
+        case .none:
+            return String(localized: "None", comment: "")
+        }
+    }
+
+    /// The UUID for the datafield application in Garmin Connect IQ
+    var datafieldUUID: UUID? {
+        switch self {
+        case .trio:
+            // return UUID(uuidString: "71cf0982-ca41-42a5-8441-ea81d36056c3")  // local build
+            // return UUID(uuidString: "f07f4ef9-108b-4397-95c9-217b5173412e")  // ConnectIQ test build
+            return UUID(uuidString: "3d9b6528-8c84-459a-bbab-989b5f001ebd") // ConnectIQ live build
+        case .swissalpine:
+            // return UUID(uuidString: "7A2268F6-3381-4474-81BD-0A3E7F458CB7") // ConnectIQ test build
+            return UUID(uuidString: "dec5292a-74b0-41bc-8e45-cd93f1d5e137") // ConnectIQ live build
+        case .none:
+            return nil
+        }
+    }
+}
+
+// MARK: - Garmin Watch Settings Group
+
+/// Groups related Garmin watch settings together for easier management.
+/// Both watchfaces use the same settings: primaryAttributeChoice and secondaryAttributeChoice.
+struct GarminWatchSettings: Codable, Hashable {
+    var watchface: GarminWatchface = .trio
+    var datafield: GarminDatafield = .trio
+    var primaryAttributeChoice: GarminPrimaryAttributeChoice = .cob
+    var secondaryAttributeChoice: GarminSecondaryAttributeChoice = .tbr
+    var isWatchfaceDataEnabled: Bool = false
+}

+ 118 - 17
Trio/Sources/Models/GarminWatchState.swift

@@ -7,35 +7,136 @@
 import Foundation
 import Foundation
 import SwiftUI
 import SwiftUI
 
 
+// MARK: - Unified Garmin Watch State
+
+/// Unified watch state structure for both Trio and SwissAlpine watchfaces.
+/// Uses the SwissAlpine xDrip+ compatible data format.
+/// Sent as an array where the first entry contains all extended data fields.
 struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
 struct GarminWatchState: Hashable, Equatable, Sendable, Encodable {
-    var glucose: String?
-    var trendRaw: String?
-    var delta: String?
-    var iob: String?
-    var cob: String?
-    var lastLoopDateInterval: UInt64?
-    var eventualBGRaw: String?
-    var isf: String?
+    /// Timestamp of the enacted loop determination in milliseconds since Unix epoch
+    /// Shows when the loop actually executed, used to indicate loop staleness
+    var date: UInt64?
+
+    /// Timestamp of the glucose reading in milliseconds since Unix epoch
+    /// Used by watchface to determine glucose freshness for coloring logic
+    var glucoseDate: UInt64?
+
+    /// Sensor glucose value in raw mg/dL (no unit conversion applied)
+    var sgv: Int16?
+
+    /// Change in glucose since previous reading as an integer
+    var delta: Int16?
+
+    /// Glucose trend direction (e.g., "Flat", "FortyFiveUp", "SingleUp")
+    var direction: String?
+
+    /// Signal noise level (optional, typically not used)
+    var noise: Double?
+
+    /// Unit hint for the watchface ("mgdl" or "mmol")
+    var units_hint: String?
+
+    /// Insulin on board as a decimal value (only in first array entry)
+    var iob: Double?
+
+    /// Current temp basal rate in U/hr (only in first array entry)
+    var tbr: Double?
+
+    /// Carbs on board as a decimal value (only in first array entry)
+    var cob: Double?
+
+    /// Predicted eventual blood glucose (excluded if data type 2 is set to TBR)
+    var eventualBG: Int16?
+
+    /// Current insulin sensitivity factor as an integer (only in first array entry)
+    var isf: Int16?
+
+    /// AutoISF sensitivity ratio (included only if data type 1 is set to sensRatio)
+    var sensRatio: Double?
+
+    // MARK: - Display Configuration Fields
+
+    /// Specifies which primary attribute to display
+    /// Options: "cob", "isf", or "sensRatio"
+    var displayPrimaryAttributeChoice: String?
+
+    /// Specifies which secondary attribute to display
+    /// Options: "tbr" or "eventualBG"
+    var displaySecondaryAttributeChoice: String?
 
 
     static func == (lhs: GarminWatchState, rhs: GarminWatchState) -> Bool {
     static func == (lhs: GarminWatchState, rhs: GarminWatchState) -> Bool {
-        lhs.glucose == rhs.glucose &&
-            lhs.trendRaw == rhs.trendRaw &&
+        lhs.date == rhs.date &&
+            lhs.glucoseDate == rhs.glucoseDate &&
+            lhs.sgv == rhs.sgv &&
             lhs.delta == rhs.delta &&
             lhs.delta == rhs.delta &&
+            lhs.direction == rhs.direction &&
+            lhs.noise == rhs.noise &&
+            lhs.units_hint == rhs.units_hint &&
             lhs.iob == rhs.iob &&
             lhs.iob == rhs.iob &&
+            lhs.tbr == rhs.tbr &&
             lhs.cob == rhs.cob &&
             lhs.cob == rhs.cob &&
-            lhs.lastLoopDateInterval == rhs.lastLoopDateInterval &&
-            lhs.eventualBGRaw == rhs.eventualBGRaw &&
-            lhs.isf == rhs.isf
+            lhs.eventualBG == rhs.eventualBG &&
+            lhs.isf == rhs.isf &&
+            lhs.sensRatio == rhs.sensRatio &&
+            lhs.displayPrimaryAttributeChoice == rhs.displayPrimaryAttributeChoice &&
+            lhs.displaySecondaryAttributeChoice == rhs.displaySecondaryAttributeChoice
     }
     }
 
 
     func hash(into hasher: inout Hasher) {
     func hash(into hasher: inout Hasher) {
-        hasher.combine(glucose)
-        hasher.combine(trendRaw)
+        hasher.combine(date)
+        hasher.combine(glucoseDate)
+        hasher.combine(sgv)
         hasher.combine(delta)
         hasher.combine(delta)
+        hasher.combine(direction)
+        hasher.combine(noise)
+        hasher.combine(units_hint)
         hasher.combine(iob)
         hasher.combine(iob)
+        hasher.combine(tbr)
         hasher.combine(cob)
         hasher.combine(cob)
-        hasher.combine(lastLoopDateInterval)
-        hasher.combine(eventualBGRaw)
+        hasher.combine(eventualBG)
         hasher.combine(isf)
         hasher.combine(isf)
+        hasher.combine(sensRatio)
+        hasher.combine(displayPrimaryAttributeChoice)
+        hasher.combine(displaySecondaryAttributeChoice)
+    }
+
+    enum CodingKeys: String, CodingKey {
+        case date
+        case glucoseDate
+        case sgv
+        case delta
+        case direction
+        case noise
+        case units_hint
+        case iob
+        case tbr
+        case cob
+        case eventualBG
+        case isf
+        case sensRatio
+        case displayPrimaryAttributeChoice
+        case displaySecondaryAttributeChoice
+    }
+
+    /// Custom encoding that excludes nil values from the JSON output
+    /// Double values are rounded to 2 decimal places to prevent floating point artifacts
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encodeIfPresent(date, forKey: .date)
+        try container.encodeIfPresent(glucoseDate, forKey: .glucoseDate)
+        try container.encodeIfPresent(sgv, forKey: .sgv)
+        try container.encodeIfPresent(delta, forKey: .delta)
+        try container.encodeIfPresent(direction, forKey: .direction)
+        try container.encodeIfPresent(noise, forKey: .noise)
+        try container.encodeIfPresent(units_hint, forKey: .units_hint)
+        // Round Double values to 2 decimal places to prevent floating point artifacts like "0.5600000000000001"
+        try container.encodeIfPresent(iob?.roundedDouble(toPlaces: 2), forKey: .iob)
+        try container.encodeIfPresent(tbr?.roundedDouble(toPlaces: 2), forKey: .tbr)
+        try container.encodeIfPresent(cob, forKey: .cob)
+        try container.encodeIfPresent(eventualBG, forKey: .eventualBG)
+        try container.encodeIfPresent(isf, forKey: .isf)
+        try container.encodeIfPresent(sensRatio?.roundedDouble(toPlaces: 2), forKey: .sensRatio)
+        try container.encodeIfPresent(displayPrimaryAttributeChoice, forKey: .displayPrimaryAttributeChoice)
+        try container.encodeIfPresent(displaySecondaryAttributeChoice, forKey: .displaySecondaryAttributeChoice)
     }
     }
 }
 }

+ 64 - 0
Trio/Sources/Models/NotificationIdentifiers.swift

@@ -0,0 +1,64 @@
+import Foundation
+import UserNotifications
+
+enum NotificationCategoryIdentifier: String {
+    case trioAlert = "Trio.alert"
+}
+
+enum NotificationResponseAction: String, CaseIterable {
+    case snooze20 = "Trio.snooze20"
+    case snooze1hr = "Trio.snooze1hr"
+    case snooze3hr = "Trio.snooze3hr"
+    case snooze6hr = "Trio.snooze6hr"
+
+    var duration: TimeInterval {
+        TimeInterval(minutes) * 60
+    }
+
+    var minutes: Int {
+        switch self {
+        case .snooze20:
+            return 20
+        case .snooze1hr:
+            return 60
+        case .snooze3hr:
+            return 180
+        case .snooze6hr:
+            return 360
+        }
+    }
+
+    var localizedTitle: String {
+        switch self {
+        case .snooze20:
+            return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes")
+        case .snooze1hr:
+            return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour")
+        case .snooze3hr:
+            return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours")
+        case .snooze6hr:
+            return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours")
+        }
+    }
+}
+
+// MARK: - NotificationCategoryFactory
+
+enum NotificationCategoryFactory {
+    static func createGlucoseCategory() -> UNNotificationCategory {
+        let snoozeActions = NotificationResponseAction.allCases.map { action in
+            UNNotificationAction(
+                identifier: action.rawValue,
+                title: action.localizedTitle,
+                options: []
+            )
+        }
+
+        return UNNotificationCategory(
+            identifier: NotificationCategoryIdentifier.trioAlert.rawValue,
+            actions: snoozeActions,
+            intentIdentifiers: [],
+            options: []
+        )
+    }
+}

+ 86 - 8
Trio/Sources/Models/TrioSettings.swift

@@ -15,7 +15,7 @@ enum BolusShortcutLimit: String, JSON, CaseIterable, Identifiable {
     }
     }
 }
 }
 
 
-struct TrioSettings: JSON, Equatable {
+struct TrioSettings: JSON, Equatable, Encodable {
     var units: GlucoseUnits = .mgdL
     var units: GlucoseUnits = .mgdL
     var closedLoop: Bool = false
     var closedLoop: Bool = false
     var isUploadEnabled: Bool = false
     var isUploadEnabled: Bool = false
@@ -40,9 +40,8 @@ struct TrioSettings: JSON, Equatable {
     var highGlucose: Decimal = 270
     var highGlucose: Decimal = 270
     var carbsRequiredThreshold: Decimal = 10
     var carbsRequiredThreshold: Decimal = 10
     var showCarbsRequiredBadge: Bool = true
     var showCarbsRequiredBadge: Bool = true
-    var useFPUconversion: Bool = true
+    var useFPUconversion: Bool = false
     var individualAdjustmentFactor: Decimal = 0.5
     var individualAdjustmentFactor: Decimal = 0.5
-    var timeCap: Decimal = 8
     var minuteInterval: Decimal = 30
     var minuteInterval: Decimal = 30
     var delay: Decimal = 60
     var delay: Decimal = 60
     var useAppleHealth: Bool = false
     var useAppleHealth: Bool = false
@@ -53,7 +52,12 @@ struct TrioSettings: JSON, Equatable {
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var yGridLines: Bool = true
+    var hideInsulinBadge: Bool = false
+    var allowDilution: Bool = false
+    var insulinConcentration: Decimal = 1
+    var showCobIobChart: Bool = true
     var rulerMarks: Bool = true
     var rulerMarks: Bool = true
+    var bolusDisplayThreshold: BolusDisplayThreshold = .allUnits
     var forecastDisplayType: ForecastDisplayType = .cone
     var forecastDisplayType: ForecastDisplayType = .cone
     var maxCarbs: Decimal = 250
     var maxCarbs: Decimal = 250
     var maxFat: Decimal = 250
     var maxFat: Decimal = 250
@@ -71,10 +75,43 @@ struct TrioSettings: JSON, Equatable {
     var smartStackView: LockScreenView = .simple
     var smartStackView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var timeInRangeType: TimeInRangeType = .timeInTightRange
     var timeInRangeType: TimeInRangeType = .timeInTightRange
+
+    /// Selected Garmin watchface (Trio or SwissAlpine)
+    var garminWatchface: GarminWatchface = .trio
+    var garminDatafield: GarminDatafield = .none
+
+    /// Primary attribute choice for Garmin display (COB, ISF, or Sensitivity Ratio)
+    var primaryAttributeChoice: GarminPrimaryAttributeChoice = .cob
+
+    /// Secondary attribute choice for Garmin display (TBR or Eventual BG)
+    var secondaryAttributeChoice: GarminSecondaryAttributeChoice = .tbr
+
+    /// Controls whether watchface data transmission is enabled
+    var isWatchfaceDataEnabled: Bool = false
+
+    /// Computed property that groups all Garmin settings into a single struct
+    var garminSettings: GarminWatchSettings {
+        get {
+            GarminWatchSettings(
+                watchface: garminWatchface,
+                datafield: garminDatafield,
+                primaryAttributeChoice: primaryAttributeChoice,
+                secondaryAttributeChoice: secondaryAttributeChoice,
+                isWatchfaceDataEnabled: isWatchfaceDataEnabled
+            )
+        }
+        set {
+            garminWatchface = newValue.watchface
+            garminDatafield = newValue.datafield
+            primaryAttributeChoice = newValue.primaryAttributeChoice
+            secondaryAttributeChoice = newValue.secondaryAttributeChoice
+            isWatchfaceDataEnabled = newValue.isWatchfaceDataEnabled
+        }
+    }
 }
 }
 
 
 extension TrioSettings: Decodable {
 extension TrioSettings: Decodable {
-    // Needed to decode incomplete JSON
+    /// Custom decoder to handle incomplete JSON and provide default values for missing fields
     init(from decoder: Decoder) throws {
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         let container = try decoder.container(keyedBy: CodingKeys.self)
         var settings = TrioSettings()
         var settings = TrioSettings()
@@ -167,10 +204,6 @@ extension TrioSettings: Decodable {
             settings.overrideFactor = overrideFactor
             settings.overrideFactor = overrideFactor
         }
         }
 
 
-        if let timeCap = try? container.decode(Decimal.self, forKey: .timeCap) {
-            settings.timeCap = timeCap
-        }
-
         if let minuteInterval = try? container.decode(Decimal.self, forKey: .minuteInterval) {
         if let minuteInterval = try? container.decode(Decimal.self, forKey: .minuteInterval) {
             settings.minuteInterval = minuteInterval
             settings.minuteInterval = minuteInterval
         }
         }
@@ -249,10 +282,30 @@ extension TrioSettings: Decodable {
             settings.yGridLines = yGridLines
             settings.yGridLines = yGridLines
         }
         }
 
 
+        if let showCobIobChart = try? container.decode(Bool.self, forKey: .showCobIobChart) {
+            settings.showCobIobChart = showCobIobChart
+        }
+
+        if let hideInsulinBadge = try? container.decode(Bool.self, forKey: .hideInsulinBadge) {
+            settings.hideInsulinBadge = hideInsulinBadge
+        }
+
+        if let allowDilution = try? container.decode(Bool.self, forKey: .allowDilution) {
+            settings.allowDilution = allowDilution
+        }
+
+        if let insulinConcentration = try? container.decode(Decimal.self, forKey: .insulinConcentration) {
+            settings.insulinConcentration = insulinConcentration
+        }
+
         if let rulerMarks = try? container.decode(Bool.self, forKey: .rulerMarks) {
         if let rulerMarks = try? container.decode(Bool.self, forKey: .rulerMarks) {
             settings.rulerMarks = rulerMarks
             settings.rulerMarks = rulerMarks
         }
         }
 
 
+        if let bolusDisplayThreshold = try? container.decode(BolusDisplayThreshold.self, forKey: .bolusDisplayThreshold) {
+            settings.bolusDisplayThreshold = bolusDisplayThreshold
+        }
+
         if let forecastDisplayType = try? container.decode(ForecastDisplayType.self, forKey: .forecastDisplayType) {
         if let forecastDisplayType = try? container.decode(ForecastDisplayType.self, forKey: .forecastDisplayType) {
             settings.forecastDisplayType = forecastDisplayType
             settings.forecastDisplayType = forecastDisplayType
         }
         }
@@ -305,6 +358,31 @@ extension TrioSettings: Decodable {
             settings.timeInRangeType = timeInRangeType
             settings.timeInRangeType = timeInRangeType
         }
         }
 
 
+        if let garminWatchface = try? container.decode(GarminWatchface.self, forKey: .garminWatchface) {
+            settings.garminWatchface = garminWatchface
+        }
+
+        if let garminDatafield = try? container.decode(GarminDatafield.self, forKey: .garminDatafield) {
+            settings.garminDatafield = garminDatafield
+        }
+
+        if let primaryAttributeChoice = try? container
+            .decode(GarminPrimaryAttributeChoice.self, forKey: .primaryAttributeChoice)
+        {
+            settings.primaryAttributeChoice = primaryAttributeChoice
+        }
+
+        if let secondaryAttributeChoice = try? container.decode(
+            GarminSecondaryAttributeChoice.self,
+            forKey: .secondaryAttributeChoice
+        ) {
+            settings.secondaryAttributeChoice = secondaryAttributeChoice
+        }
+
+        if let isWatchfaceDataEnabled = try? container.decode(Bool.self, forKey: .isWatchfaceDataEnabled) {
+            settings.isWatchfaceDataEnabled = isWatchfaceDataEnabled
+        }
+
         self = settings
         self = settings
     }
     }
 }
 }

+ 3 - 0
Trio/Sources/Models/WatchMessageKeys.swift

@@ -51,4 +51,7 @@ enum WatchMessageKeys {
     static let maxProtein = "maxProtein"
     static let maxProtein = "maxProtein"
     static let bolusIncrement = "bolusIncrement"
     static let bolusIncrement = "bolusIncrement"
     static let confirmBolusFaster = "confirmBolusFaster"
     static let confirmBolusFaster = "confirmBolusFaster"
+
+    // Notification Actions
+    static let snoozeDuration = "snoozeDuration"
 }
 }

+ 24 - 5
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -113,17 +113,36 @@ extension CGMSettings {
                         units: state.units,
                         units: state.units,
                         type: .boolean,
                         type: .boolean,
                         label: String(localized: "Smooth Glucose Value"),
                         label: String(localized: "Smooth Glucose Value"),
-                        miniHint: String(localized: "Smooth CGM readings using Savitzky-Golay filtering."),
+                        miniHint: String(localized: "Smooth CGM readings using exponential smoothing."),
                         verboseHint: VStack(alignment: .leading, spacing: 10) {
                         verboseHint: VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
                             Text("Default: OFF").bold()
+
                             Text(
                             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."
+                                "This feature smooths your CGM readings to reduce noise and make them easier to read. It is based on a method used in AndroidAPS (AAPS). It uses two approaches: one that reacts quickly to recent changes, and one that looks at longer trends. These are combined to give a balanced result."
                             )
                             )
+
                             Text(
                             Text(
-                                "It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed. This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels."
+                                "Trio will always display values based on your actual (raw) CGM readings. Smoothing does not change your real values or alerts."
                             )
                             )
+
+                            Text("When this feature is enabled:")
+
+                            VStack(alignment: .leading) {
+                                Text(
+                                    "• The main chart and treatment chart show a light gray trend line for the smoothed values. The glucose dots always show your original CGM readings."
+                                )
+
+                                Text("• In Trio history, you will see the smoothed value next to the original reading.")
+
+                                Text("• When you long-press a chart, the pop-up will show both the original and smoothed values.")
+                            }
+
+                            Text(
+                                "It can handle small gaps in data and ignores sensor error values. It needs at least 4 readings within 12 minutes to work properly. Only CGM readings are smoothed—manual entries are not changed."
+                            )
+
                             Text(
                             Text(
-                                "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app."
+                                "This helps Trio make more stable dosing decisions by avoiding over-reactions to small or short-term changes. Important trends are kept, while unreliable fluctuations are filtered out."
                             )
                             )
                         }
                         }
                     )
                     )
@@ -181,7 +200,7 @@ extension CGMSettings {
                         hintDetent: $hintDetent,
                         hintDetent: $hintDetent,
                         shouldDisplayHint: $shouldDisplayHint,
                         shouldDisplayHint: $shouldDisplayHint,
                         hintLabel: hintLabel ?? "",
                         hintLabel: hintLabel ?? "",
-                        hintText: AnyView(
+                        hintText: selectedVerboseHint ?? AnyView(
                             VStack(alignment: .leading, spacing: 10) {
                             VStack(alignment: .leading, spacing: 10) {
                                 Text(
                                 Text(
                                     "Current CGM Models Supported:"
                                     "Current CGM Models Supported:"

+ 21 - 3
Trio/Sources/Modules/History/View/HistoryRootView.swift

@@ -469,10 +469,11 @@ extension History {
         private var glucoseList: some View {
         private var glucoseList: some View {
             List {
             List {
                 HStack {
                 HStack {
-                    Text("Values").foregroundStyle(.secondary)
+                    Text("Values")
                     Spacer()
                     Spacer()
-                    Text("Time").foregroundStyle(.secondary)
-                }
+                    Text("Time")
+                }.foregroundStyle(.secondary)
+
                 if !glucoseStored.isEmpty {
                 if !glucoseStored.isEmpty {
                     ForEach(glucoseStored) { glucose in
                     ForEach(glucoseStored) { glucose in
                         HStack {
                         HStack {
@@ -485,6 +486,23 @@ extension History {
                                 Text("\(glucose.directionEnum?.symbol ?? "--")")
                                 Text("\(glucose.directionEnum?.symbol ?? "--")")
                             }
                             }
 
 
+                            if state.settingsManager.settings.smoothGlucose, !glucose.isManual,
+                               let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0
+                            {
+                                let smoothedGlucoseForDisplay = state.units == .mgdL ? smoothedGlucose
+                                    .description : smoothedGlucose.decimalValue
+                                    .formattedAsMmolL
+
+                                (
+                                    Text("(") +
+                                        Text(Image(systemName: "sparkles")) +
+                                        Text(" ") +
+                                        Text("\(smoothedGlucoseForDisplay)") +
+                                        Text(")")
+                                ).foregroundStyle(.secondary)
+                                    .padding(.leading, 10)
+                            }
+
                             Spacer()
                             Spacer()
 
 
                             Text(Formatter.dateFormatter.string(from: glucose.date ?? Date()))
                             Text(Formatter.dateFormatter.string(from: glucose.date ?? Date()))

+ 9 - 0
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -48,6 +48,7 @@ extension Home {
         var reservoir: Decimal?
         var reservoir: Decimal?
         var pumpName = ""
         var pumpName = ""
         var pumpExpiresAtDate: Date?
         var pumpExpiresAtDate: Date?
+        var pumpActivatedAtDate: Date?
         var highTTraisesSens: Bool = false
         var highTTraisesSens: Bool = false
         var lowTTlowersSens: Bool = false
         var lowTTlowersSens: Bool = false
         var isExerciseModeActive: Bool = false
         var isExerciseModeActive: Bool = false
@@ -76,6 +77,7 @@ extension Home {
         var displayXgridLines: Bool = false
         var displayXgridLines: Bool = false
         var displayYgridLines: Bool = false
         var displayYgridLines: Bool = false
         var thresholdLines: Bool = false
         var thresholdLines: Bool = false
+        var bolusDisplayThreshold: BolusDisplayThreshold = .allUnits
         var hours: Int16 = 6
         var hours: Int16 = 6
         var totalBolus: Decimal = 0
         var totalBolus: Decimal = 0
         var isLoopStatusPresented: Bool = false
         var isLoopStatusPresented: Bool = false
@@ -344,6 +346,11 @@ extension Home {
                 .weakAssign(to: \.pumpExpiresAtDate, on: self)
                 .weakAssign(to: \.pumpExpiresAtDate, on: self)
                 .store(in: &lifetime)
                 .store(in: &lifetime)
 
 
+            apsManager.pumpActivatedAtDate
+                .receive(on: DispatchQueue.main)
+                .weakAssign(to: \.pumpActivatedAtDate, on: self)
+                .store(in: &lifetime)
+
             apsManager.lastError
             apsManager.lastError
                 .receive(on: DispatchQueue.main)
                 .receive(on: DispatchQueue.main)
                 .map { [weak self] error in
                 .map { [weak self] error in
@@ -404,6 +411,7 @@ extension Home {
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             displayXgridLines = settingsManager.settings.xGridLines
             displayXgridLines = settingsManager.settings.xGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             displayYgridLines = settingsManager.settings.yGridLines
+            bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
             thresholdLines = settingsManager.settings.rulerMarks
             thresholdLines = settingsManager.settings.rulerMarks
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             forecastDisplayType = settingsManager.settings.forecastDisplayType
@@ -668,6 +676,7 @@ extension Home.StateModel:
         displayXgridLines = settingsManager.settings.xGridLines
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
         thresholdLines = settingsManager.settings.rulerMarks
+        bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
         showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
         showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)

+ 16 - 2
Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift

@@ -177,6 +177,7 @@ extension MainChartView {
         let startOfDay = Calendar.current.startOfDay(for: beginDate)
         let startOfDay = Calendar.current.startOfDay(for: beginDate)
         let profile = state.basalProfile
         let profile = state.basalProfile
         var basalPoints: [BasalProfile] = []
         var basalPoints: [BasalProfile] = []
+        var lastEntryBeforeRange: (amount: Double, date: Date)?
 
 
         // Iterate over the next three days, multiplying the time intervals
         // Iterate over the next three days, multiplying the time intervals
         for dayOffset in 0 ..< 3 {
         for dayOffset in 0 ..< 3 {
@@ -185,8 +186,12 @@ extension MainChartView {
                 let basalTime = startOfDay.addingTimeInterval(entry.minutes.minutes.timeInterval + dayTimeOffset)
                 let basalTime = startOfDay.addingTimeInterval(entry.minutes.minutes.timeInterval + dayTimeOffset)
                 let basalTimeInterval = basalTime.timeIntervalSince1970
                 let basalTimeInterval = basalTime.timeIntervalSince1970
 
 
-                // Only append points within the timeBegin and timeEnd range
-                if basalTimeInterval >= timeBegin, basalTimeInterval < timeEnd {
+                if basalTimeInterval < timeBegin {
+                    // Track the last profile entry before the visible range
+                    if lastEntryBeforeRange == nil || basalTime > lastEntryBeforeRange!.date {
+                        lastEntryBeforeRange = (amount: Double(entry.rate), date: basalTime)
+                    }
+                } else if basalTimeInterval < timeEnd {
                     basalPoints.append(BasalProfile(
                     basalPoints.append(BasalProfile(
                         amount: Double(entry.rate),
                         amount: Double(entry.rate),
                         isOverwritten: false,
                         isOverwritten: false,
@@ -196,6 +201,15 @@ extension MainChartView {
             }
             }
         }
         }
 
 
+        // Include the active profile entry at timeBegin so the line starts at the chart's left edge
+        if let lastBefore = lastEntryBeforeRange {
+            basalPoints.append(BasalProfile(
+                amount: lastBefore.amount,
+                isOverwritten: false,
+                startDate: beginDate
+            ))
+        }
+
         return basalPoints
         return basalPoints
     }
     }
 
 

+ 27 - 37
Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift

@@ -32,46 +32,36 @@ struct GlucoseChartView: ChartContent {
                 glucoseColorScheme: glucoseColorScheme
                 glucoseColorScheme: glucoseColorScheme
             )
             )
 
 
-            if !isSmoothingEnabled {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", glucoseToDisplay)
-                )
-                .foregroundStyle(pointMarkColor)
-                .symbolSize(20)
-                .symbol {
-                    if item.isManual {
-                        Image(systemName: "drop.fill")
-                            .font(.caption2)
-                            .symbolRenderingMode(.monochrome)
-                            .bold()
-                            .foregroundStyle(.red)
-                    } else {
-                        Image(systemName: "circle.fill")
-                            .font(.system(size: 5))
-                            .bold()
-                            .foregroundStyle(pointMarkColor)
-                    }
+            PointMark(
+                x: .value("Time", item.date ?? Date(), unit: .second),
+                y: .value("Value", glucoseToDisplay)
+            )
+            .foregroundStyle(pointMarkColor)
+            .symbolSize(20)
+            .symbol {
+                if item.isManual {
+                    Image(systemName: "drop.fill")
+                        .font(.caption2)
+                        .symbolRenderingMode(.monochrome)
+                        .bold()
+                        .foregroundStyle(.red)
+                } else {
+                    Image(systemName: "circle.fill")
+                        .font(.system(size: 5))
+                        .bold()
+                        .foregroundStyle(pointMarkColor)
                 }
                 }
-            } else {
-                PointMark(
+            }
+
+            if isSmoothingEnabled, let smoothedGlucose = item.smoothedGlucose, smoothedGlucose != 0 {
+                let smoothedGlucoseForDisplay: Decimal = units == .mgdL ? smoothedGlucose.decimalValue : smoothedGlucose
+                    .decimalValue.asMmolL
+                LineMark(
                     x: .value("Time", item.date ?? Date(), unit: .second),
                     x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", glucoseToDisplay)
+                    y: .value("Value", smoothedGlucoseForDisplay),
+                    series: .value("Type", "Smoothed")
                 )
                 )
-                .symbol {
-                    if item.isManual {
-                        Image(systemName: "drop.fill")
-                            .font(.caption2)
-                            .symbolRenderingMode(.monochrome)
-                            .bold()
-                            .foregroundStyle(.red)
-                    } else {
-                        Image(systemName: "record.circle.fill")
-                            .font(.system(size: 8))
-                            .bold()
-                            .foregroundStyle(pointMarkColor)
-                    }
-                }
+                .foregroundStyle(Color.secondary)
             }
             }
         }
         }
     }
     }

+ 6 - 3
Trio/Sources/Modules/Home/View/Chart/ChartElements/InsulinView.swift

@@ -6,6 +6,7 @@ struct InsulinView: ChartContent {
     let glucoseData: [GlucoseStored]
     let glucoseData: [GlucoseStored]
     let insulinData: [PumpEventStored]
     let insulinData: [PumpEventStored]
     let units: GlucoseUnits
     let units: GlucoseUnits
+    let bolusDisplayThreshold: BolusDisplayThreshold
 
 
     var body: some ChartContent {
     var body: some ChartContent {
         drawBoluses()
         drawBoluses()
@@ -32,9 +33,11 @@ struct InsulinView: ChartContent {
                     Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.insulin)
                     Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.insulin)
                 }
                 }
                 .annotation(position: .top) {
                 .annotation(position: .top) {
-                    Text(Formatter.bolusFormatter.string(from: amount) ?? "")
-                        .font(.caption2)
-                        .foregroundStyle(Color.primary)
+                    if amount as Decimal >= bolusDisplayThreshold.rawValue {
+                        Text(Formatter.bolusFormatter.string(from: amount) ?? "")
+                            .font(.caption2)
+                            .foregroundStyle(Color.primary)
+                    }
                 }
                 }
             }
             }
         }
         }

+ 14 - 3
Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift

@@ -11,6 +11,7 @@ struct SelectionPopoverView: ChartContent {
     let lowGlucose: Decimal
     let lowGlucose: Decimal
     let currentGlucoseTarget: Decimal
     let currentGlucoseTarget: Decimal
     let glucoseColorScheme: GlucoseColorScheme
     let glucoseColorScheme: GlucoseColorScheme
+    let isSmoothingEnabled: Bool
 
 
     private var glucoseToDisplay: Decimal {
     private var glucoseToDisplay: Decimal {
         units == .mgdL ? Decimal(selectedGlucose.glucose) : Decimal(selectedGlucose.glucose).asMmolL
         units == .mgdL ? Decimal(selectedGlucose.glucose) : Decimal(selectedGlucose.glucose).asMmolL
@@ -33,7 +34,7 @@ struct SelectionPopoverView: ChartContent {
     var body: some ChartContent {
     var body: some ChartContent {
         RuleMark(x: .value("Selection", selectedGlucose.date ?? Date.now, unit: .minute))
         RuleMark(x: .value("Selection", selectedGlucose.date ?? Date.now, unit: .minute))
             .foregroundStyle(Color.tabBar)
             .foregroundStyle(Color.tabBar)
-            .offset(yStart: 70)
+            .offset(yStart: isSmoothingEnabled ? 90 : 70)
             .lineStyle(.init(lineWidth: 2))
             .lineStyle(.init(lineWidth: 2))
             .annotation(
             .annotation(
                 position: .top,
                 position: .top,
@@ -70,15 +71,25 @@ struct SelectionPopoverView: ChartContent {
             .font(.body).padding(.bottom, 2)
             .font(.body).padding(.bottom, 2)
 
 
             HStack {
             HStack {
-                Text(glucoseToDisplay.description).bold() + Text(" \(units.rawValue)")
+                Text("CGM: ") + Text(glucoseToDisplay.description).bold() + Text(" \(units.rawValue)")
             }
             }
             .foregroundStyle(pointMarkColor)
             .foregroundStyle(pointMarkColor)
             .font(.body)
             .font(.body)
 
 
+            if isSmoothingEnabled, let smoothedGlucose = selectedGlucose.smoothedGlucose {
+                var smoothedGlucoseToDisplay: Decimal {
+                    units == .mgdL ? smoothedGlucose.decimalValue : smoothedGlucose.decimalValue.asMmolL
+                }
+                HStack {
+                    Image(systemName: "sparkles")
+                    Text(smoothedGlucoseToDisplay.description) + Text(" \(units.rawValue)")
+                }.font(.body)
+            }
+
             if let selectedIOBValue, let iob = selectedIOBValue.iob {
             if let selectedIOBValue, let iob = selectedIOBValue.iob {
                 HStack {
                 HStack {
                     Image(systemName: "syringe.fill").frame(width: 15)
                     Image(systemName: "syringe.fill").frame(width: 15)
-                    Text(Formatter.bolusFormatter.string(from: iob) ?? "")
+                    Text(Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob) ?? "")
                         .bold()
                         .bold()
                         + Text(String(localized: " U", comment: "Insulin unit"))
                         + Text(String(localized: " U", comment: "Insulin unit"))
                 }
                 }

+ 4 - 2
Trio/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -149,7 +149,8 @@ extension MainChartView {
                 InsulinView(
                 InsulinView(
                     glucoseData: state.glucoseFromPersistence,
                     glucoseData: state.glucoseFromPersistence,
                     insulinData: state.insulinFromPersistence,
                     insulinData: state.insulinFromPersistence,
-                    units: state.units
+                    units: state.units,
+                    bolusDisplayThreshold: state.bolusDisplayThreshold
                 )
                 )
 
 
                 CarbView(
                 CarbView(
@@ -181,7 +182,8 @@ extension MainChartView {
                         highGlucose: highGlucose,
                         highGlucose: highGlucose,
                         lowGlucose: lowGlucose,
                         lowGlucose: lowGlucose,
                         currentGlucoseTarget: currentGlucoseTarget,
                         currentGlucoseTarget: currentGlucoseTarget,
-                        glucoseColorScheme: glucoseColorScheme
+                        glucoseColorScheme: glucoseColorScheme,
+                        isSmoothingEnabled: state.settingsManager.settings.smoothGlucose
                     )
                     )
                 }
                 }
             }
             }

+ 45 - 25
Trio/Sources/Modules/Home/View/Header/PumpView.swift

@@ -5,11 +5,14 @@ struct PumpView: View {
     let reservoir: Decimal?
     let reservoir: Decimal?
     let name: String
     let name: String
     let expiresAtDate: Date?
     let expiresAtDate: Date?
+    let activatedAtDate: Date?
     let timerDate: Date
     let timerDate: Date
     let pumpStatusHighlightMessage: String?
     let pumpStatusHighlightMessage: String?
     let battery: [OpenAPS_Battery]
     let battery: [OpenAPS_Battery]
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
 
 
+    let NORMAL_PATCH_AGE = TimeInterval.hours(80)
+
     private var batteryFormatter: NumberFormatter {
     private var batteryFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
         formatter.numberStyle = .percent
         formatter.numberStyle = .percent
@@ -17,6 +20,7 @@ struct PumpView: View {
     }
     }
 
 
     private var hourglassIcon: String {
     private var hourglassIcon: String {
+        if activatedAtDate != nil { return "hourglass.badge.plus" }
         guard let expiration = expiresAtDate else { return "hourglass" }
         guard let expiration = expiresAtDate else { return "hourglass" }
 
 
         let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600
         let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600
@@ -96,34 +100,38 @@ struct PumpView: View {
                 }
                 }
 
 
                 if let date = expiresAtDate {
                 if let date = expiresAtDate {
-                    HStack {
-                        Image(systemName: hourglassIcon)
-                            .font(.callout)
-                            .foregroundStyle(timerColor, Color.yellow)
-                            .symbolRenderingMode(.palette)
-
-                        let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate))
-
-                        Text(remainingTimeString)
-                            .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline)
-                            .fontWeight(.bold)
-                            .fontDesign(.rounded)
-                            .lineLimit(2)
-                            .multilineTextAlignment(.leading)
-                            .frame(
-                                // If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts
-                                // This forces the "Replace pod" string to wrap to 2 lines.
-                                maxWidth: remainingTimeString.count > 6 ? 80 : .infinity,
-                                alignment: .leading
-                            )
-                    }
-                    // aligns the stopwatch icon exactly with the first pixel of the reservoir icon
-                    .padding(.leading, date.timeIntervalSince(timerDate) > 0 ? 12 : 0)
+                    PatchTimer(forDate: date)
                 }
                 }
             }
             }
         }
         }
     }
     }
 
 
+    @ViewBuilder private func PatchTimer(forDate date: Date) -> some View {
+        HStack {
+            Image(systemName: hourglassIcon)
+                .font(.callout)
+                .foregroundStyle(timerColor, timerColorSecondary)
+                .symbolRenderingMode(.palette)
+
+            let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate))
+
+            Text(remainingTimeString)
+                .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline)
+                .fontWeight(.bold)
+                .fontDesign(.rounded)
+                .lineLimit(2)
+                .multilineTextAlignment(.leading)
+                .frame(
+                    // If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts
+                    // This forces the "Replace pod" string to wrap to 2 lines.
+                    maxWidth: remainingTimeString.count > 6 ? 80 : .infinity,
+                    alignment: .leading
+                )
+        }
+        // aligns the stopwatch icon exactly with the first pixel of the reservoir icon
+        .padding(.leading, date.timeIntervalSince(timerDate) > 0 || activatedAtDate != nil ? 12 : 0)
+    }
+
     private func remainingTimeString(time: TimeInterval) -> String {
     private func remainingTimeString(time: TimeInterval) -> String {
         guard time > 0 else {
         guard time > 0 else {
             return String(localized: "Replace pod", comment: "View/Header when pod expired")
             return String(localized: "Replace pod", comment: "View/Header when pod expired")
@@ -184,11 +192,15 @@ struct PumpView: View {
     }
     }
 
 
     private var timerColor: Color {
     private var timerColor: Color {
-        guard let expisesAt = expiresAtDate else {
+        if let activatedAt = activatedAtDate {
+            return abs(activatedAt.timeIntervalSinceNow) > NORMAL_PATCH_AGE ? Color.yellow : Color.loopGreen
+        }
+
+        guard let expiresAt = expiresAtDate else {
             return .gray
             return .gray
         }
         }
 
 
-        let time = expisesAt.timeIntervalSince(timerDate)
+        let time = expiresAt.timeIntervalSince(timerDate)
 
 
         switch time {
         switch time {
         case ...8.hours.timeInterval:
         case ...8.hours.timeInterval:
@@ -199,6 +211,14 @@ struct PumpView: View {
             return Color.loopGreen
             return Color.loopGreen
         }
         }
     }
     }
+
+    private var timerColorSecondary: Color {
+        if activatedAtDate != nil {
+            return Color.gray
+        }
+
+        return Color.yellow
+    }
 }
 }
 
 
 // #Preview("message") {
 // #Preview("message") {

+ 2 - 1
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -153,6 +153,7 @@ extension Home {
                 reservoir: state.reservoir,
                 reservoir: state.reservoir,
                 name: state.pumpName,
                 name: state.pumpName,
                 expiresAtDate: state.pumpExpiresAtDate,
                 expiresAtDate: state.pumpExpiresAtDate,
+                activatedAtDate: state.pumpActivatedAtDate,
                 timerDate: state.timerDate,
                 timerDate: state.timerDate,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 battery: state.batteryFromPersistence
                 battery: state.batteryFromPersistence
@@ -1003,7 +1004,6 @@ extension Home {
             }
             }
             .navigationTitle("Home")
             .navigationTitle("Home")
             .navigationBarHidden(true)
             .navigationBarHidden(true)
-            .ignoresSafeArea(.keyboard)
             .blur(radius: state.isLoopStatusPresented ? 3 : 0)
             .blur(radius: state.isLoopStatusPresented ? 3 : 0)
             .sheet(isPresented: $state.isLoopStatusPresented) {
             .sheet(isPresented: $state.isLoopStatusPresented) {
                 LoopStatusView(state: state)
                 LoopStatusView(state: state)
@@ -1017,6 +1017,7 @@ extension Home {
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
                 Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                 Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
+                Button("Medtrum Nano") { state.addPump(.medtrum) }
                 Button("Pump Simulator") { state.addPump(.simulator) }
                 Button("Pump Simulator") { state.addPump(.simulator) }
             } message: { Text("Select Pump Model") }
             } message: { Text("Select Pump Model") }
             .sheet(isPresented: $state.shouldDisplayPumpSetupSheet) {
             .sheet(isPresented: $state.shouldDisplayPumpSetupSheet) {

+ 2 - 1
Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -143,7 +143,8 @@ extension ISFEditor {
         private var isfChart: some View {
         private var isfChart: some View {
             Chart {
             Chart {
                 ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
                 ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
-                    let displayValue = state.rateValues[item.rateIndex]
+                    let displayValue = state.units == .mgdL ? state.rateValues[item.rateIndex] : state.rateValues[item.rateIndex]
+                        .asMmolL
 
 
                     let startDate = Calendar.current
                     let startDate = Calendar.current
                         .startOfDay(for: now)
                         .startOfDay(for: now)

+ 1 - 5
Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift

@@ -3,12 +3,11 @@ import SwiftUI
 extension MealSettings {
 extension MealSettings {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
         @Published var units: GlucoseUnits = .mgdL
         @Published var units: GlucoseUnits = .mgdL
-        @Published var useFPUconversion: Bool = true
+        @Published var useFPUconversion: Bool = false
         @Published var maxCarbs: Decimal = 250
         @Published var maxCarbs: Decimal = 250
         @Published var maxFat: Decimal = 250
         @Published var maxFat: Decimal = 250
         @Published var maxProtein: Decimal = 250
         @Published var maxProtein: Decimal = 250
         @Published var individualAdjustmentFactor: Decimal = 0.5
         @Published var individualAdjustmentFactor: Decimal = 0.5
-        @Published var timeCap: Decimal = 8
         @Published var minuteInterval: Decimal = 30
         @Published var minuteInterval: Decimal = 30
         @Published var delay: Decimal = 60
         @Published var delay: Decimal = 60
         @Published var maxMealAbsorptionTime: Decimal = 6
         @Published var maxMealAbsorptionTime: Decimal = 6
@@ -27,9 +26,6 @@ extension MealSettings {
             // "Fat and Protein Delay"
             // "Fat and Protein Delay"
             subscribeSetting(\.delay, on: $delay) { delay = $0 }
             subscribeSetting(\.delay, on: $delay) { delay = $0 }
 
 
-            // "Maximum Duration"
-            subscribeSetting(\.timeCap, on: $timeCap) { timeCap = $0 }
-
             // "Spread Interval"
             // "Spread Interval"
             subscribeSetting(\.minuteInterval, on: $minuteInterval) { minuteInterval = $0 }
             subscribeSetting(\.minuteInterval, on: $minuteInterval) { minuteInterval = $0 }
 
 

+ 4 - 34
Trio/Sources/Modules/MealSettings/View/MealSettingsRootView.swift

@@ -203,7 +203,7 @@ extension MealSettings {
                     VStack(alignment: .leading, spacing: 10) {
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 6 hours").bold()
                         Text("Default: 6 hours").bold()
                         Text(
                         Text(
-                            "Carb entries will be fully decayed by the number of hours specified as Max Meal Absorption Time. Meals that are high in fat and/or protein can have long lasting effects on BG levels. To allow such late meal effects to be considered by the carb decay model, a longer Max Meal Absorption Time than the default 6 hours can be set."
+                            "Carb entries will be fully decayed by the number of hours specified as Max Meal Absorption Time. Meals that are high in fat and/or protein can have long lasting effects on glucose levels. To allow such late meal effects to be considered by the carb decay model, a longer Max Meal Absorption Time than the default 6 hours can be set."
                         )
                         )
                         Text(
                         Text(
                             "If carb entries decay too slowly, it is possible to set a lower than default setting. But this should typically be adressed by tuning ISF and CR settings instead, which in combination determines the rate of carb decay."
                             "If carb entries decay too slowly, it is possible to set a lower than default setting. But this should typically be adressed by tuning ISF and CR settings instead, which in combination determines the rate of carb decay."
@@ -258,7 +258,6 @@ extension MealSettings {
                                     "You can personalize the conversion calculation by adjusting the following settings that will appear when this option is enabled:"
                                     "You can personalize the conversion calculation by adjusting the following settings that will appear when this option is enabled:"
                                 )
                                 )
                                 Text("• Fat and Protein Delay")
                                 Text("• Fat and Protein Delay")
-                                Text("• Maximum Duration")
                                 Text("• Spread Interval")
                                 Text("• Spread Interval")
                                 Text("• Fat and Protein Percentage")
                                 Text("• Fat and Protein Percentage")
                             }
                             }
@@ -295,35 +294,6 @@ extension MealSettings {
                     )
                     )
 
 
                     SettingInputSection(
                     SettingInputSection(
-                        decimalValue: $state.timeCap,
-                        booleanValue: $booleanPlaceholder,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Maximum Duration")
-                            }
-                        ),
-                        units: state.units,
-                        type: .decimal("timeCap"),
-                        label: String(localized: "Maximum Duration"),
-                        miniHint: String(localized: "Set the maximum timeframe to extend FPUs."),
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: 8 hours").bold()
-                            Text(
-                                "This sets the maximum length of time that Fat and Protein Carb Equivalents (FPUs) will be extended over from a single Fat and/or Protein bolus calcultor entry."
-                            )
-                            Text(
-                                "It is one factor used in combination with the Fat and Protein Delay, Spread Interval, and Fat and Protein Factor to create the FPU entries."
-                            )
-                            Text("Increasing this setting may result in more FPU entries with smaller carb values.")
-                            Text("Decreasing this setting may result in fewer FPU entries with larger carb values.")
-                        }
-                    )
-
-                    SettingInputSection(
                         decimalValue: $state.minuteInterval,
                         decimalValue: $state.minuteInterval,
                         booleanValue: $booleanPlaceholder,
                         booleanValue: $booleanPlaceholder,
                         shouldDisplayHint: $shouldDisplayHint,
                         shouldDisplayHint: $shouldDisplayHint,
@@ -344,9 +314,9 @@ extension MealSettings {
                             Text(
                             Text(
                                 "This determines how many minutes will be between individual Fat-Protein Unit Carb Equivalent (FPU) entries from a single Fat and/or Protein bolus calculator entry."
                                 "This determines how many minutes will be between individual Fat-Protein Unit Carb Equivalent (FPU) entries from a single Fat and/or Protein bolus calculator entry."
                             )
                             )
-                            Text("The shorter the interval, the smoother the correlating dosing result.")
-                            Text("Increasing this setting may result in fewer FPU entries with larger carb values.")
-                            Text("Decreasing this setting may result in more FPU entries with smaller carb values.")
+                            Text(
+                                "Entries are capped at 33 grams each, with up to three entries, for a max total of 99 grams."
+                            )
                         }
                         }
                     )
                     )
 
 

+ 6 - 2
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -3,6 +3,7 @@ import DanaKit
 import FirebaseCrashlytics
 import FirebaseCrashlytics
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
+import MedtrumKit
 import MinimedKit
 import MinimedKit
 import Observation
 import Observation
 import OmniBLE
 import OmniBLE
@@ -44,7 +45,7 @@ extension Onboarding {
 
 
         // MARK: - Determine Initial Build State
         // MARK: - Determine Initial Build State
 
 
-        /// Determines whether the app is in a fresh install state for Trio v0.3.0.
+        /// Determines whether the app is in a fresh install state for Trio (new vs. returning/updating user).
         ///
         ///
         /// This check is based on the assumption that a truly clean install will only contain
         /// This check is based on the assumption that a truly clean install will only contain
         /// the `logs/` directory and the `preferences.json` file in the app's Documents directory.
         /// the `logs/` directory and the `preferences.json` file in the app's Documents directory.
@@ -120,6 +121,8 @@ extension Onboarding {
                         defaultOption = .omnipodDash
                         defaultOption = .omnipodDash
                     } else if pumpManager is OmnipodPumpManager {
                     } else if pumpManager is OmnipodPumpManager {
                         defaultOption = .omnipodEros
                         defaultOption = .omnipodEros
+                    } else if pumpManager is MedtrumPumpManager {
+                        defaultOption = .medtrum
                     } else if pumpManager is DanaKitPumpManager {
                     } else if pumpManager is DanaKitPumpManager {
                         defaultOption = .dana
                         defaultOption = .dana
                     } else if pumpManager is MinimedPumpManager {
                     } else if pumpManager is MinimedPumpManager {
@@ -165,6 +168,8 @@ extension Onboarding {
                 return PickerSetting(value: 0.1, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour)
                 return PickerSetting(value: 0.1, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour)
             case .omnipodEros:
             case .omnipodEros:
                 return PickerSetting(value: 0.1, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour)
                 return PickerSetting(value: 0.1, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour)
+            case .medtrum:
+                return PickerSetting(value: 0.1, step: 0.05, min: 0.05, max: 30, type: .insulinUnitPerHour)
             case .none:
             case .none:
                 // same as dash, as that is the fallback
                 // same as dash, as that is the fallback
                 return PickerSetting(value: 0.1, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour)
                 return PickerSetting(value: 0.1, step: 0.05, min: 0, max: 30, type: .insulinUnitPerHour)
@@ -717,7 +722,6 @@ extension Onboarding {
                     .clamp(to: providedSettings.carbsRequiredThreshold)
                     .clamp(to: providedSettings.carbsRequiredThreshold)
                 settingsCopy.individualAdjustmentFactor = settingsCopy.individualAdjustmentFactor
                 settingsCopy.individualAdjustmentFactor = settingsCopy.individualAdjustmentFactor
                     .clamp(to: providedSettings.individualAdjustmentFactor)
                     .clamp(to: providedSettings.individualAdjustmentFactor)
-                settingsCopy.timeCap = settingsCopy.timeCap.clamp(to: providedSettings.timeCap)
                 settingsCopy.minuteInterval = settingsCopy.minuteInterval.clamp(to: providedSettings.minuteInterval)
                 settingsCopy.minuteInterval = settingsCopy.minuteInterval.clamp(to: providedSettings.minuteInterval)
                 settingsCopy.delay = settingsCopy.delay.clamp(to: providedSettings.delay)
                 settingsCopy.delay = settingsCopy.delay.clamp(to: providedSettings.delay)
                 settingsCopy.high = settingsCopy.high.clamp(to: providedSettings.high)
                 settingsCopy.high = settingsCopy.high.clamp(to: providedSettings.high)

+ 2 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift

@@ -597,7 +597,8 @@ struct OnboardingNavigationButtons: View {
                 case .dana,
                 case .dana,
                      .minimed:
                      .minimed:
                     currentAutosensSubstep = .rewindResetsAutosens
                     currentAutosensSubstep = .rewindResetsAutosens
-                case .omnipodDash,
+                case .medtrum,
+                     .omnipodDash,
                      .omnipodEros:
                      .omnipodEros:
                     currentAutosensSubstep = .autosensMax
                     currentAutosensSubstep = .autosensMax
                 }
                 }

+ 2 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsSubstepView.swift

@@ -21,7 +21,8 @@ struct AlgorithmSettingsSubstepView<Substep: AlgorithmSubstepProtocol & RawRepre
         case .dana,
         case .dana,
              .minimed:
              .minimed:
             return false
             return false
-        case .omnipodDash,
+        case .medtrum,
+             .omnipodDash,
              .omnipodEros:
              .omnipodEros:
             return true
             return true
         }
         }

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift

@@ -121,8 +121,8 @@ enum AlgorithmSettingsSubstep: Int, CaseIterable, Identifiable {
         case .maxSMBMinutes: return String(localized: "Max SMB Basal Minutes", comment: "Max SMB Basal Minutes")
         case .maxSMBMinutes: return String(localized: "Max SMB Basal Minutes", comment: "Max SMB Basal Minutes")
         case .maxUAMMinutes: return String(localized: "Max UAM Basal Minutes", comment: "Max UAM Basal Minutes")
         case .maxUAMMinutes: return String(localized: "Max UAM Basal Minutes", comment: "Max UAM Basal Minutes")
         case .maxDeltaGlucoseThreshold: return String(
         case .maxDeltaGlucoseThreshold: return String(
-                localized: "Max. Allowed Glucose Rise for SMB",
-                comment: "Max. Allowed Glucose Rise for SMB, formerly Max Delta-BG Threshold"
+                localized: "Max Allowed Glucose Rise for SMB",
+                comment: "Max Allowed Glucose Rise for SMB, formerly Max Delta-BG Threshold"
             )
             )
         case .highTempTargetRaisesSensitivity: return String(
         case .highTempTargetRaisesSensitivity: return String(
                 localized: "High Temp Target Raises Sensitivity",
                 localized: "High Temp Target Raises Sensitivity",

+ 3 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -504,6 +504,7 @@ enum PumpOptionForOnboardingUnits: String, Equatable, CaseIterable, Identifiable
     case omnipodEros
     case omnipodEros
     case omnipodDash
     case omnipodDash
     case dana
     case dana
+    case medtrum
 
 
     var id: String { rawValue }
     var id: String { rawValue }
 
 
@@ -517,6 +518,8 @@ enum PumpOptionForOnboardingUnits: String, Equatable, CaseIterable, Identifiable
             return "Omnipod DASH"
             return "Omnipod DASH"
         case .dana:
         case .dana:
             return "Dana (RS/-i)"
             return "Dana (RS/-i)"
+        case .medtrum:
+            return "Medtrum Nano"
         }
         }
     }
     }
 }
 }

+ 1 - 0
Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift

@@ -10,6 +10,7 @@ enum PumpConfig {
         case omnipod
         case omnipod
         case omnipodBLE
         case omnipodBLE
         case dana
         case dana
+        case medtrum
         case simulator
         case simulator
     }
     }
 
 

+ 5 - 0
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -36,6 +36,9 @@ extension PumpConfig {
                                 } label: {
                                 } label: {
                                     HStack {
                                     HStack {
                                         Image(uiImage: pumpState.image ?? UIImage())
                                         Image(uiImage: pumpState.image ?? UIImage())
+                                            .resizable()
+                                            .scaledToFit()
+                                            .frame(maxWidth: 100)
                                         Text(pumpState.name)
                                         Text(pumpState.name)
                                     }
                                     }
                                     .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
                                     .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
@@ -118,6 +121,7 @@ extension PumpConfig {
                                     Text("• Omnipod Eros")
                                     Text("• Omnipod Eros")
                                     Text("• Omnipod DASH")
                                     Text("• Omnipod DASH")
                                     Text("• Dana (RS/-i)")
                                     Text("• Dana (RS/-i)")
+                                    Text("• Medtrum Nano (200u/300u)")
                                     Text("• Pump Simulator")
                                     Text("• Pump Simulator")
                                 }
                                 }
                                 Text(
                                 Text(
@@ -133,6 +137,7 @@ extension PumpConfig {
                     Button("Omnipod Eros") { state.addPump(.omnipod) }
                     Button("Omnipod Eros") { state.addPump(.omnipod) }
                     Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                     Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                     Button("Dana(RS/-i)") { state.addPump(.dana) }
                     Button("Dana(RS/-i)") { state.addPump(.dana) }
+                    Button("Medtrum Nano") { state.addPump(.medtrum) }
                     Button("Pump Simulator") { state.addPump(.simulator) }
                     Button("Pump Simulator") { state.addPump(.simulator) }
                 } message: { Text("Select Pump Model") }
                 } message: { Text("Select Pump Model") }
             }
             }

+ 10 - 0
Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -1,6 +1,7 @@
 import DanaKit
 import DanaKit
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
+import MedtrumKit
 import MinimedKit
 import MinimedKit
 import MinimedKitUI
 import MinimedKitUI
 import MockKit
 import MockKit
@@ -68,6 +69,15 @@ extension PumpConfig {
                     prefersToSkipUserInteraction: false,
                     prefersToSkipUserInteraction: false,
                     allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                     allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
                 )
+            case .medtrum:
+                setupViewController = MedtrumPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: true,
+                    prefersToSkipUserInteraction: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
+                )
             case .simulator:
             case .simulator:
                 setupViewController = MockPumpManager.setupViewController(
                 setupViewController = MockPumpManager.setupViewController(
                     initialSettings: initialSettings,
                     initialSettings: initialSettings,

+ 4 - 4
Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift

@@ -331,16 +331,16 @@ extension SMBSettings {
                         set: {
                         set: {
                             selectedVerboseHint = $0.map { AnyView($0) }
                             selectedVerboseHint = $0.map { AnyView($0) }
                             hintLabel = String(
                             hintLabel = String(
-                                localized: "Max. Allowed Glucose Rise for SMB",
-                                comment: "Max. Allowed Glucose Rise for SMB, formerly Max Delta-BG Threshold"
+                                localized: "Max Allowed Glucose Rise for SMB",
+                                comment: "Max Allowed Glucose Rise for SMB, formerly Max Delta-BG Threshold"
                             )
                             )
                         }
                         }
                     ),
                     ),
                     units: state.units,
                     units: state.units,
                     type: .decimal("maxDeltaBGthreshold"),
                     type: .decimal("maxDeltaBGthreshold"),
                     label: String(
                     label: String(
-                        localized: "Max. Allowed Glucose Rise for SMB",
-                        comment: "Max. Allowed Glucose Rise for SMB, formerly Max Delta-BG Threshold"
+                        localized: "Max Allowed Glucose Rise for SMB",
+                        comment: "Max Allowed Glucose Rise for SMB, formerly Max Delta-BG Threshold"
                     ),
                     ),
                     miniHint: String(localized: "Disables SMBs if last two glucose values differ by more than this percent."),
                     miniHint: String(localized: "Disables SMBs if last two glucose values differ by more than this percent."),
                     verboseHint:
                     verboseHint:

+ 7 - 7
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -114,13 +114,13 @@ enum SettingItems {
                 "Enable SMB With COB",
                 "Enable SMB With COB",
                 "Enable SMB With Temporary Target",
                 "Enable SMB With Temporary Target",
                 "Enable SMB After Carbs",
                 "Enable SMB After Carbs",
-                "Enable SMB With High BG",
-                "High BG Target",
+                "Enable SMB With High Glucose",
+                "High Glucose Target",
                 "Allow SMB With High Temporary Target",
                 "Allow SMB With High Temporary Target",
                 "Enable UAM",
                 "Enable UAM",
                 "Max SMB Basal Minutes",
                 "Max SMB Basal Minutes",
                 "Max UAM SMB Basal Minutes",
                 "Max UAM SMB Basal Minutes",
-                "Max Delta-BG Threshold SMB"
+                "Max Allowed Glucose Rise for SMB"
             ],
             ],
             path: ["Algorithm", "Super Micro Bolus (SMB)"]
             path: ["Algorithm", "Super Micro Bolus (SMB)"]
         ),
         ),
@@ -195,11 +195,10 @@ enum SettingItems {
                 "Max Meal Absorption Time",
                 "Max Meal Absorption Time",
                 "Max Fat",
                 "Max Fat",
                 "Max Protein",
                 "Max Protein",
-                "Display and Allow Fat and Protein Entries",
+                "Enable Fat and Protein Entries",
                 "Fat and Protein Delay",
                 "Fat and Protein Delay",
-                "Maximum Duration (hours)",
-                "Spread Interval (minutes)",
-                "Fat and Protein Factor",
+                "Spread Interval",
+                "Fat and Protein Percentage",
                 "FPU"
                 "FPU"
             ],
             ],
             path: ["Features", "Meal Settings"]
             path: ["Features", "Meal Settings"]
@@ -230,6 +229,7 @@ enum SettingItems {
                 "Show Carbs Required Badge",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
                 "Carbs Required Threshold",
                 "Forecast Display Type",
                 "Forecast Display Type",
+                "Bolus Display Threshold",
                 "Cone",
                 "Cone",
                 "Lines",
                 "Lines",
                 "Dark Mode",
                 "Dark Mode",

+ 1 - 8
Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift

@@ -396,7 +396,7 @@ extension SettingsExport {
                 addSetting(
                 addSetting(
                     category: algorithmCategory,
                     category: algorithmCategory,
                     subcategory: smbSubcategory,
                     subcategory: smbSubcategory,
-                    name: String(localized: "Max. Allowed Glucose Rise for SMB"),
+                    name: String(localized: "Max Allowed Glucose Rise for SMB"),
                     value: String(format: "%.0f", (preferences.maxDeltaBGthreshold as NSDecimalNumber).doubleValue * 100),
                     value: String(format: "%.0f", (preferences.maxDeltaBGthreshold as NSDecimalNumber).doubleValue * 100),
                     unit: "%"
                     unit: "%"
                 )
                 )
@@ -704,13 +704,6 @@ extension SettingsExport {
                 addSetting(
                 addSetting(
                     category: featuresCategory,
                     category: featuresCategory,
                     subcategory: mealSettingsSubcategory,
                     subcategory: mealSettingsSubcategory,
-                    name: String(localized: "Maximum Duration"),
-                    value: String(describing: trioSettings.timeCap),
-                    unit: String(localized: "hours")
-                )
-                addSetting(
-                    category: featuresCategory,
-                    subcategory: mealSettingsSubcategory,
                     name: String(localized: "Spread Interval"),
                     name: String(localized: "Spread Interval"),
                     value: String(describing: trioSettings.minuteInterval),
                     value: String(describing: trioSettings.minuteInterval),
                     unit: String(localized: "minutes")
                     unit: String(localized: "minutes")

+ 31 - 2
Trio/Sources/Modules/Snooze/SnoozeStateModel.swift

@@ -4,12 +4,41 @@ import SwiftUI
 extension Snooze {
 extension Snooze {
     @Observable final class StateModel: BaseStateModel<Provider> {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast
         @ObservationIgnored @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast
-        @ObservationIgnored @Injected() var glucoseStogare: GlucoseStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var notificationsManager: UserNotificationsManager!
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
 
 
         var alarm: GlucoseAlarm?
         var alarm: GlucoseAlarm?
 
 
         override func subscribe() {
         override func subscribe() {
-            alarm = glucoseStogare.alarm
+            alarm = glucoseStorage.alarm
+            broadcaster.register(SnoozeObserver.self, observer: self)
         }
         }
+
+        func unsubscribe() {
+            broadcaster.unregister(SnoozeObserver.self, observer: self)
+        }
+
+        // Add validation helper inside the class
+        private func validateSnoozeDuration(_ duration: TimeInterval) -> Bool {
+            // Only allow durations matching our defined actions
+            NotificationResponseAction.allCases
+                .map(\.duration)
+                .contains(duration)
+        }
+
+        @MainActor func applySnooze(_ duration: TimeInterval) async {
+            // Allow any duration chosen in the Snooze UI, while keeping validation for quick actions elsewhere.
+            snoozeUntilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast
+            alarm = glucoseStorage.alarm
+            await notificationsManager.applySnooze(for: duration)
+        }
+    }
+}
+
+extension Snooze.StateModel: SnoozeObserver {
+    func snoozeDidChange(_ untilDate: Date) {
+        snoozeUntilDate = untilDate
+        alarm = glucoseStorage.alarm
     }
     }
 }
 }

+ 26 - 30
Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift

@@ -14,29 +14,12 @@ extension Snooze {
         @State private var snoozeDescription = "nothing to see here"
         @State private var snoozeDescription = "nothing to see here"
 
 
         private var pickerTimes: [TimeInterval] {
         private var pickerTimes: [TimeInterval] {
-            var arr: [TimeInterval] = []
-
-            let mins10 = 0.166_67
-            let mins20 = mins10 * 2
-            let mins30 = mins10 * 3
-            // let mins40 = mins10 * 4
-
-            for hr in 0 ..< 2 {
-                for min in [0.0, mins20, mins20 * 2] {
-                    arr.append(TimeInterval(hours: Double(hr) + min))
-                }
-            }
-            for hr in 2 ..< 4 {
-                for min in [0.0, mins30] {
-                    arr.append(TimeInterval(hours: Double(hr) + min))
-                }
-            }
-
-            for hr in 4 ... 8 {
-                arr.append(TimeInterval(hours: Double(hr)))
-            }
-
-            return arr
+            [
+                TimeInterval(minutes: 20), // 20 minutes
+                TimeInterval(hours: 1), // 1 hour
+                TimeInterval(hours: 3), // 3 hours
+                TimeInterval(hours: 6) // 6 hours
+            ]
         }
         }
 
 
         private var formatter: DateComponentsFormatter {
         private var formatter: DateComponentsFormatter {
@@ -53,7 +36,7 @@ extension Snooze {
         }
         }
 
 
         private func formatInterval(_ interval: TimeInterval) -> String {
         private func formatInterval(_ interval: TimeInterval) -> String {
-            formatter.string(from: interval)!
+            formatter.string(from: interval) ?? ""
         }
         }
 
 
         func getSnoozeDescription() -> String {
         func getSnoozeDescription() -> String {
@@ -85,12 +68,16 @@ extension Snooze {
             VStack(alignment: .leading) {
             VStack(alignment: .leading) {
                 Button {
                 Button {
                     let interval = pickerTimes[selectedInterval]
                     let interval = pickerTimes[selectedInterval]
-                    let snoozeFor = formatter.string(from: interval)!
+                    let snoozeFor = formatInterval(interval)
                     let untilDate = Date() + interval
                     let untilDate = Date() + interval
-                    state.snoozeUntilDate = untilDate < Date() ? .distantPast : untilDate
-                    debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))")
-                    snoozeDescription = getSnoozeDescription()
-                    state.hideModal()
+
+                    Task { @MainActor [weak state] in
+                        guard let state = state else { return }
+                        await state.applySnooze(interval)
+                        debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))")
+                        snoozeDescription = getSnoozeDescription()
+                        state.hideModal()
+                    }
                 } label: {
                 } label: {
                     Text("Click to Snooze Alerts")
                     Text("Click to Snooze Alerts")
                         .padding()
                         .padding()
@@ -120,11 +107,20 @@ extension Snooze {
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .navigationBarTitle("Snooze Alerts")
             .navigationBarTitle("Snooze Alerts")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
-            .navigationBarItems(trailing: Button("Close", action: state.hideModal))
+            .toolbar {
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button("Close") {
+                        state.hideModal()
+                    }
+                }
+            }
             .onAppear {
             .onAppear {
                 configureView()
                 configureView()
                 snoozeDescription = getSnoozeDescription()
                 snoozeDescription = getSnoozeDescription()
             }
             }
+            .onDisappear {
+                state.unsubscribe()
+            }
         }
         }
     }
     }
 }
 }

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

@@ -343,7 +343,7 @@ struct GlucoseSectorChart: View {
                         formatPercentage(Decimal(low) / total * 100)
                         formatPercentage(Decimal(low) / total * 100)
                     ),
                     ),
                     (
                     (
-                        String(localized: "Very Low (<\(Decimal(54).formatted(for: units))"),
+                        String(localized: "Very Low (<\(Decimal(54).formatted(for: units)))"),
                         formatPercentage(Decimal(veryLow) / total * 100)
                         formatPercentage(Decimal(veryLow) / total * 100)
                     ),
                     ),
                     (String(localized: "Average"), average.formatted(for: units)),
                     (String(localized: "Average"), average.formatted(for: units)),

+ 2 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -272,9 +272,9 @@ struct BolusStatsView: View {
                             AxisGridLine()
                             AxisGridLine()
                         }
                         }
                     case .total:
                     case .total:
-                        // Only show every other month
+                        // Show start of every month
                         let day = Calendar.current.component(.day, from: date)
                         let day = Calendar.current.component(.day, from: date)
-                        if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
+                        if day == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                                 .font(.footnote)
                             AxisGridLine()
                             AxisGridLine()

+ 2 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -224,8 +224,8 @@ struct TotalDailyDoseChart: View {
                             AxisGridLine()
                             AxisGridLine()
                         }
                         }
                     case .total:
                     case .total:
-                        // Only show every other month
-                        if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
+                        // Show start of every month
+                        if day == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                                 .font(.footnote)
                             AxisGridLine()
                             AxisGridLine()

+ 2 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -254,9 +254,9 @@ struct MealStatsView: View {
                             AxisGridLine()
                             AxisGridLine()
                         }
                         }
                     case .total:
                     case .total:
-                        // Only show every other month
+                        // Show start of every month
                         let day = Calendar.current.component(.day, from: date)
                         let day = Calendar.current.component(.day, from: date)
-                        if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
+                        if day == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                                 .font(.footnote)
                             AxisGridLine()
                             AxisGridLine()

+ 39 - 17
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -135,7 +135,7 @@ struct ForecastChart: View {
                     .lineStyle(.init(lineWidth: 2))
                     .lineStyle(.init(lineWidth: 2))
                     .annotation(
                     .annotation(
                         position: .top,
                         position: .top,
-                        overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
+                        overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
                     ) {
                     ) {
                         selectionPopover
                         selectionPopover
                     }
                     }
@@ -221,12 +221,22 @@ struct ForecastChart: View {
                     glucoseColorScheme: state.glucoseColorScheme
                     glucoseColorScheme: state.glucoseColorScheme
                 )
                 )
                 HStack {
                 HStack {
-                    Text(state.units == .mgdL ? Decimal(sgv).description : Decimal(sgv).formattedAsMmolL)
+                    Text("CGM: ") + Text(state.units == .mgdL ? Decimal(sgv).description : Decimal(sgv).formattedAsMmolL)
                         .bold()
                         .bold()
                         + Text(" \(state.units.rawValue)")
                         + Text(" \(state.units.rawValue)")
                 }.foregroundStyle(
                 }.foregroundStyle(
                     Color(glucoseColor)
                     Color(glucoseColor)
                 ).font(.footnote)
                 ).font(.footnote)
+
+                if state.isSmoothingEnabled, let smoothedGlucose = selectedGlucose?.smoothedGlucose {
+                    let smoothedGlucoseToDisplay: Decimal = state.units == .mgdL
+                        ? smoothedGlucose.decimalValue
+                        : smoothedGlucose.decimalValue.asMmolL
+                    HStack {
+                        Image(systemName: "sparkles")
+                        Text(smoothedGlucoseToDisplay.description) + Text(" \(state.units.rawValue)")
+                    }.font(.footnote)
+                }
             }
             }
             .padding(7)
             .padding(7)
             .background {
             .background {
@@ -259,25 +269,37 @@ struct ForecastChart: View {
                 glucoseColorScheme: state.glucoseColorScheme
                 glucoseColorScheme: state.glucoseColorScheme
             )
             )
 
 
-            if !state.isSmoothingEnabled {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", glucoseToDisplay)
-                )
-                .foregroundStyle(pointMarkColor)
-                .symbolSize(18)
-            } else {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", glucoseToDisplay)
-                )
-                .symbol {
-                    Image(systemName: "record.circle.fill")
-                        .font(.system(size: 6))
+            PointMark(
+                x: .value("Time", item.date ?? Date(), unit: .second),
+                y: .value("Value", glucoseToDisplay)
+            )
+            .foregroundStyle(pointMarkColor)
+            .symbol {
+                if item.isManual {
+                    Image(systemName: "drop.fill")
+                        .font(.caption2)
+                        .symbolRenderingMode(.monochrome)
+                        .bold()
+                        .foregroundStyle(.red)
+                } else {
+                    Image(systemName: "circle.fill")
+                        .font(.system(size: 4))
                         .bold()
                         .bold()
                         .foregroundStyle(pointMarkColor)
                         .foregroundStyle(pointMarkColor)
                 }
                 }
             }
             }
+
+            if state.isSmoothingEnabled, let smoothedGlucose = item.smoothedGlucose, smoothedGlucose != 0 {
+                let smoothedGlucoseForDisplay: Decimal = state.units == .mgdL
+                    ? smoothedGlucose.decimalValue
+                    : smoothedGlucose.decimalValue.asMmolL
+                LineMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", smoothedGlucoseForDisplay),
+                    series: .value("Type", "Smoothed")
+                )
+                .foregroundStyle(Color.secondary)
+            }
         }
         }
     }
     }
 
 

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

@@ -7,6 +7,7 @@ extension UserInterfaceSettings {
         @Published var xGridLines = false
         @Published var xGridLines = false
         @Published var yGridLines: Bool = false
         @Published var yGridLines: Bool = false
         @Published var rulerMarks: Bool = true
         @Published var rulerMarks: Bool = true
+        @Published var bolusDisplayThreshold: BolusDisplayThreshold = .allUnits
         @Published var forecastDisplayType: ForecastDisplayType = .cone
         @Published var forecastDisplayType: ForecastDisplayType = .cone
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var carbsRequiredThreshold: Decimal = 0
@@ -23,6 +24,7 @@ extension UserInterfaceSettings {
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
+            subscribeSetting(\.bolusDisplayThreshold, on: $bolusDisplayThreshold) { bolusDisplayThreshold = $0 }
 
 
             subscribeSetting(\.forecastDisplayType, on: $forecastDisplayType) { forecastDisplayType = $0 }
             subscribeSetting(\.forecastDisplayType, on: $forecastDisplayType) { forecastDisplayType = $0 }
 
 

+ 42 - 0
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -376,6 +376,48 @@ extension UserInterfaceSettings {
                     }.padding(.bottom)
                     }.padding(.bottom)
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
 
 
+                Section {
+                    VStack {
+                        Picker(
+                            selection: $state.bolusDisplayThreshold,
+                            label: Text("Bolus Display Threshold")
+                        ) {
+                            ForEach(BolusDisplayThreshold.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose to hide small bolus amounts. See hint for more details."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = String(localized: "Bolus Display Threshold")
+                                    selectedVerboseHint =
+                                        AnyView(
+                                            VStack(alignment: .leading) {
+                                                Text(
+                                                    "This setting controls which bolus amount labels are shown on Trio’s main chart. Boluses appear as blue upside-down triangles, with a number showing the amount. Depending on the option you choose, only boluses at or above that amount will show a label. For example, if you choose ‘0.5 U and over’, only boluses of 0.5 U or more will show a label."
+                                                )
+                                            }
+                                        )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
+
                 Section(
                 Section(
                     header: Text("Trio Statistics"),
                     header: Text("Trio Statistics"),
                     content: {
                     content: {

+ 274 - 0
Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminAppConfigView.swift

@@ -0,0 +1,274 @@
+import SwiftUI
+
+struct WatchConfigGarminAppConfigView: View {
+    @ObservedObject var state: WatchConfig.StateModel
+
+    @State private var shouldDisplayHint1: Bool = false
+    @State private var shouldDisplayHint2: Bool = false
+    @State private var shouldDisplayHint3: Bool = false
+    @State private var shouldDisplayHint4: Bool = false
+    @State var hintDetent = PresentationDetent.large
+    @State private var shouldShowWatchfaceSwitchConfirmDialog: Bool = false
+
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    var body: some View {
+        Form {
+            // MARK: - Watchface Selection Section
+
+            Section(
+                header: Text("Watchface Settings"),
+                content: {
+                    VStack {
+                        Picker(
+                            selection: $state.garminSettings.watchface,
+                            label: Text("Watchface Selection").multilineTextAlignment(.leading)
+                        ) {
+                            ForEach(GarminWatchface.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }
+                        .padding(.top)
+                        .onChange(of: state.garminSettings.watchface) { oldValue, newValue in
+                            if oldValue != newValue {
+                                state.handleWatchfaceChange()
+                                shouldShowWatchfaceSwitchConfirmDialog = true
+                            }
+                        }
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose which watchface to support."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    shouldDisplayHint1.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+
+                    VStack {
+                        // Inverted binding: "Disable" toggle controls "isEnabled" boolean
+                        // When toggle is ON → data transmission is DISABLED (isEnabled = false)
+                        // When toggle is OFF → data transmission is ENABLED (isEnabled = true)
+                        Toggle("Disable Watchface Data", isOn: Binding(
+                            get: { !state.garminSettings.isWatchfaceDataEnabled },
+                            set: { state.garminSettings.isWatchfaceDataEnabled = !$0 }
+                        ))
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose if you only want to use a datafield and no supported watchface!"
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    shouldDisplayHint2.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }
+            ).listRowBackground(Color.chart)
+
+            // MARK: - Datafield Selection Section
+
+            Section(
+                header: Text("Datafield Settings"),
+                content: {
+                    VStack {
+                        Picker(
+                            selection: $state.garminSettings.datafield,
+                            label: Text("Datafield Selection").multilineTextAlignment(.leading)
+                        ) {
+                            ForEach(GarminDatafield.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }
+                        .padding(.top)
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose which datafield to support. Can be used independently of watchface selection."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    shouldDisplayHint4.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }
+            ).listRowBackground(Color.chart)
+
+            // MARK: - Data Field Selection Section
+
+            Section(
+                header: Text("Watch App Display Settings"),
+                content: {
+                    VStack {
+                        Picker(
+                            selection: $state.garminSettings.primaryAttributeChoice,
+                            label: Text("Data Choice 1").multilineTextAlignment(.leading)
+                        ) {
+                            ForEach(GarminPrimaryAttributeChoice.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose between displayed data types on Garmin device."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    shouldDisplayHint3.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+
+                    VStack {
+                        Picker(
+                            selection: $state.garminSettings.secondaryAttributeChoice,
+                            label: Text("Data Choice 2").multilineTextAlignment(.leading)
+                        ) {
+                            ForEach(GarminSecondaryAttributeChoice.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose between displayed data types on Garmin device."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    shouldDisplayHint3.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }
+            ).listRowBackground(Color.chart)
+        }
+        .listSectionSpacing(sectionSpacing)
+        .scrollContentBackground(.hidden)
+        .background(appState.trioBackgroundColor(for: colorScheme))
+
+        // MARK: - Help Sheets
+
+        .sheet(isPresented: $shouldDisplayHint1) {
+            SettingInputHintView(
+                hintDetent: $hintDetent,
+                shouldDisplayHint: $shouldDisplayHint1,
+                hintLabel: "Choose Garmin Watchface",
+                hintText: Text(
+                    "Choose which watchface on your Garmin device you wish to provide data for. You can independently select which datafield to use in the next section.\n\n" +
+                        "• Trio – The original Trio watchface, developed by Ivan Valkou.\n" +
+                        "• Swissalpine – Originally developed for AAPS, adapted to work with Trio.\n\n" +
+                        "You must use this configuration setting here BEFORE you switch the watchface on your Garmin device to another watchface.\n\n" +
+                        "⚠️ Changing the watchface will automatically disable data transmission. You will be prompted to resume data transmission after you have changed the watchface on your Garmin device."
+                ),
+                sheetTitle: String(localized: "Help", comment: "Help sheet title")
+            )
+        }
+        .sheet(isPresented: $shouldDisplayHint4) {
+            SettingInputHintView(
+                hintDetent: $hintDetent,
+                shouldDisplayHint: $shouldDisplayHint4,
+                hintLabel: "Choose Garmin Datafield",
+                hintText: Text(
+                    "Choose which datafield on your Garmin device you wish to provide data for. The datafield can be used independently from the watchface selection.\n\n" +
+                        "• Trio – The original Trio datafield, developed by Pierre.\n" +
+                        "• Swissalpine – Originally developed for AAPS, adapted to work with Trio.\n\n" +
+                        "Select 'None' if you don't want to use a datafield, or want to preserve battery while not exercising."
+                ),
+                sheetTitle: String(localized: "Help", comment: "Help sheet title")
+            )
+        }
+        .sheet(isPresented: $shouldDisplayHint2) {
+            SettingInputHintView(
+                hintDetent: $hintDetent,
+                shouldDisplayHint: $shouldDisplayHint2,
+                hintLabel: "Disable watchface data transmission",
+                hintText: Text(
+                    "Important: If you want to use a different watchface on your Garmin device that has no data requirement from this app, disable data transmission to the Garmin watchface app! Otherwise you will not be able to get current data once you re-enable the supported watchface that shows Trio data and you will have to re-install it on your Garmin device.\n\n" +
+                        "Note: When switching between supported watchfaces, data transmission is automatically disabled. You will be prompted to resume data transmission after you have changed the watchface on your Garmin device."
+                ),
+                sheetTitle: String(localized: "Help", comment: "Help sheet title")
+            )
+        }
+        .sheet(isPresented: $shouldDisplayHint3) {
+            SettingInputHintView(
+                hintDetent: $hintDetent,
+                shouldDisplayHint: $shouldDisplayHint3,
+                hintLabel: "Choose data support",
+                hintText: Text(
+                    "Choose which data types, along with Blood Glucose and IOB etc., you want to show on your Garmin device. That data type will be shown both on watchface and datafield.\n\n" +
+                        "Data Choice 1 options:\n" +
+                        "• COB – Carbs On Board\n" +
+                        "• ISF – Insulin Sensitivity Factor\n" +
+                        "• Sens Ratio – Sensitivity Ratio\n\n" +
+                        "Data Choice 2 options:\n" +
+                        "• Temp Basal Rate\n" +
+                        "• Eventual Glucose"
+                ),
+                sheetTitle: String(localized: "Help", comment: "Help sheet title")
+            )
+        }
+        .confirmationDialog("Watchface Changed", isPresented: $shouldShowWatchfaceSwitchConfirmDialog) {
+            Button("Resume Data Transmission") {
+                state.resumeDataTransmission()
+            }
+        } message: {
+            Text(
+                "Data transmission has been disabled. Now select the new watchface on your Garmin device and resume data transmission once done."
+            )
+        }
+    }
+}

+ 121 - 16
Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminView.swift

@@ -1,37 +1,120 @@
+import ConnectIQ
 import SwiftUI
 import SwiftUI
 
 
 struct WatchConfigGarminView: View {
 struct WatchConfigGarminView: View {
     @ObservedObject var state: WatchConfig.StateModel
     @ObservedObject var state: WatchConfig.StateModel
-
+    @State private var showDeviceList = false
     @State private var shouldDisplayHint: Bool = false
     @State private var shouldDisplayHint: Bool = false
     @State var hintDetent = PresentationDetent.large
     @State var hintDetent = PresentationDetent.large
-    @State var selectedVerboseHint: AnyView?
-    @State var hintLabel: String?
-    @State private var decimalPlaceholder: Decimal = 0.0
-    @State private var booleanPlaceholder: Bool = false
 
 
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
     @Environment(AppState.self) var appState
     @Environment(AppState.self) var appState
 
 
+    /// Handles deletion of devices from the device list
     private func onDelete(offsets: IndexSet) {
     private func onDelete(offsets: IndexSet) {
         state.devices.remove(atOffsets: offsets)
         state.devices.remove(atOffsets: offsets)
         state.deleteGarminDevice()
         state.deleteGarminDevice()
     }
     }
 
 
+    #if targetEnvironment(simulator)
+        /// Adds a mock Garmin device for simulator UI testing
+        private func addMockDevice() {
+            let mockDevice = BaseGarminManager.MockIQDevice.createSimulated()
+            state.devices.append(mockDevice)
+            state.deleteGarminDevice()
+        }
+    #endif
+
     var body: some View {
     var body: some View {
+        Group {
+            if state.devices.isEmpty || showDeviceList {
+                // No devices connected OR user wants to see device list - show device list/add view
+                deviceListView
+            } else {
+                // Devices connected - go directly to configuration
+                WatchConfigGarminAppConfigView(state: state)
+                    .navigationTitle("Garmin App Settings")
+                    .navigationBarTitleDisplayMode(.automatic)
+                    .navigationBarBackButtonHidden(true)
+                    .toolbar {
+                        ToolbarItem(placement: .navigationBarLeading) {
+                            Button(action: {
+                                showDeviceList = true
+                            }) {
+                                HStack {
+                                    Image(systemName: "chevron.left")
+                                    Text("Garmin Devices")
+                                }
+                            }
+                        }
+                    }
+            }
+        }
+        .id(state.devices.count) // Force view refresh when device count changes
+        .onChange(of: state.devices.count) { _, newValue in
+            // If devices were deleted and now empty, ensure we show device list
+            if newValue == 0 {
+                showDeviceList = false
+            }
+        }
+    }
+
+    var deviceListView: some View {
         Form {
         Form {
+            #if targetEnvironment(simulator)
+
+                // MARK: - Simulator Testing
+
+                Section(
+                    header: Text("Simulator Testing"),
+                    content: {
+                        VStack {
+                            if state.devices.isEmpty {
+                                Button {
+                                    // Add a mock device for UI testing
+                                    addMockDevice()
+                                } label: {
+                                    Text("Add Mock Garmin Watch")
+                                        .font(.title3)
+                                }
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .buttonStyle(.bordered)
+                            } else {
+                                Button {
+                                    state.devices.removeAll()
+                                    state.deleteGarminDevice()
+                                } label: {
+                                    Text("Remove All Devices")
+                                        .font(.title3)
+                                }
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .buttonStyle(.bordered)
+                                .tint(.red)
+                            }
+
+                            Text("Simulator only - for testing UI workflow")
+                                .font(.caption)
+                                .foregroundColor(.orange)
+                                .padding(.top, 5)
+                        }.padding(.vertical)
+                    }
+                ).listRowBackground(Color.orange.opacity(0.2))
+            #endif
+
+            // MARK: - Device Configuration Section
+
             Section(
             Section(
                 header: Text("Garmin Configuration"),
                 header: Text("Garmin Configuration"),
-                content:
-                {
+                content: {
                     VStack {
                     VStack {
                         Button {
                         Button {
                             state.selectGarminDevices()
                             state.selectGarminDevices()
                         } label: {
                         } label: {
                             Text("Add Device")
                             Text("Add Device")
-                                .font(.title3) }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .buttonStyle(.bordered)
+                                .font(.title3)
+                        }
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .buttonStyle(.bordered)
 
 
                         HStack(alignment: .center) {
                         HStack(alignment: .center) {
                             Text(
                             Text(
@@ -56,9 +139,11 @@ struct WatchConfigGarminView: View {
                 }
                 }
             ).listRowBackground(Color.chart)
             ).listRowBackground(Color.chart)
 
 
+            // MARK: - Device List Section
+
             if !state.devices.isEmpty {
             if !state.devices.isEmpty {
                 Section(
                 Section(
-                    header: Text("Garmin Watch"),
+                    header: Text("Connected Devices"),
                     content: {
                     content: {
                         List {
                         List {
                             ForEach(state.devices, id: \.uuid) { device in
                             ForEach(state.devices, id: \.uuid) { device in
@@ -68,23 +153,43 @@ struct WatchConfigGarminView: View {
                         }
                         }
                     }
                     }
                 ).listRowBackground(Color.chart)
                 ).listRowBackground(Color.chart)
+
+                // MARK: - App Settings Navigation Section
+
+                Section(
+                    header: Text("Device App Settings"),
+                    content: {
+                        Button(action: {
+                            showDeviceList = false
+                        }) {
+                            HStack {
+                                Text("Configure Device Apps")
+                                Spacer()
+                                Image(systemName: "chevron.right")
+                                    .font(.caption)
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+                        .foregroundColor(.primary)
+                    }
+                ).listRowBackground(Color.chart)
             }
             }
         }
         }
         .listSectionSpacing(sectionSpacing)
         .listSectionSpacing(sectionSpacing)
+        .navigationTitle("Garmin Devices")
+        .navigationBarTitleDisplayMode(.automatic)
+        .scrollContentBackground(.hidden)
+        .background(appState.trioBackgroundColor(for: colorScheme))
         .sheet(isPresented: $shouldDisplayHint) {
         .sheet(isPresented: $shouldDisplayHint) {
             SettingInputHintView(
             SettingInputHintView(
                 hintDetent: $hintDetent,
                 hintDetent: $hintDetent,
                 shouldDisplayHint: $shouldDisplayHint,
                 shouldDisplayHint: $shouldDisplayHint,
                 hintLabel: "Add Device",
                 hintLabel: "Add Device",
                 hintText: Text(
                 hintText: Text(
-                    "Add Garmin Device to Trio. Please look at the docs to see which devices are supported."
+                    "Add Garmin Device to Trio. This happens via Garmin Connect. If you have multiple phones with Garmin Connect and the same Garmin device, you will run into connectivity issue between watch and phone depending of proximity of the phones, which might also affect your watchface function."
                 ),
                 ),
                 sheetTitle: String(localized: "Help", comment: "Help sheet title")
                 sheetTitle: String(localized: "Help", comment: "Help sheet title")
             )
             )
         }
         }
-        .navigationTitle("Garmin")
-        .navigationBarTitleDisplayMode(.automatic)
-        .scrollContentBackground(.hidden)
-        .background(appState.trioBackgroundColor(for: colorScheme))
     }
     }
 }
 }

+ 19 - 1
Trio/Sources/Modules/WatchConfig/WatchConfigStateModel.swift

@@ -1,3 +1,4 @@
+import Combine
 import ConnectIQ
 import ConnectIQ
 import SwiftUI
 import SwiftUI
 
 
@@ -9,18 +10,23 @@ extension WatchConfig {
         @Published var devices: [IQDevice] = []
         @Published var devices: [IQDevice] = []
         @Published var confirmBolusFaster = false
         @Published var confirmBolusFaster = false
 
 
+        /// Garmin watch settings containing all watch-related configuration
+        @Published var garminSettings = GarminWatchSettings()
+
         private(set) var preferences = Preferences()
         private(set) var preferences = Preferences()
 
 
         override func subscribe() {
         override func subscribe() {
             preferences = provider.preferences
             preferences = provider.preferences
-
             units = settingsManager.settings.units
             units = settingsManager.settings.units
 
 
+            // Subscribe to the entire garminSettings struct from TrioSettings
+            subscribeSetting(\.garminSettings, on: $garminSettings) { garminSettings = $0 }
             subscribeSetting(\.confirmBolusFaster, on: $confirmBolusFaster) { confirmBolusFaster = $0 }
             subscribeSetting(\.confirmBolusFaster, on: $confirmBolusFaster) { confirmBolusFaster = $0 }
 
 
             devices = garmin.devices
             devices = garmin.devices
         }
         }
 
 
+        /// Prompts the user to select Garmin devices and updates the device list
         func selectGarminDevices() {
         func selectGarminDevices() {
             garmin.selectDevices()
             garmin.selectDevices()
                 .receive(on: DispatchQueue.main)
                 .receive(on: DispatchQueue.main)
@@ -28,9 +34,21 @@ extension WatchConfig {
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }
 
 
+        /// Updates the Garmin manager with the current device list
         func deleteGarminDevice() {
         func deleteGarminDevice() {
             garmin.updateDeviceList(devices)
             garmin.updateDeviceList(devices)
         }
         }
+
+        /// Handles watchface selection changes by automatically disabling data transmission
+        /// to allow the user to switch watchfaces on their Garmin device without data conflicts
+        func handleWatchfaceChange() {
+            garminSettings.isWatchfaceDataEnabled = false
+        }
+
+        /// Resumes data transmission after user confirms they have switched watchface on their device
+        func resumeDataTransmission() {
+            garminSettings.isWatchfaceDataEnabled = true
+        }
     }
     }
 }
 }
 
 

+ 112 - 13
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -11,6 +11,7 @@ import UserNotifications
 protocol UserNotificationsManager {
 protocol UserNotificationsManager {
     func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Void)
     func getNotificationSettings(completionHandler: @escaping (UNNotificationSettings) -> Void)
     func requestNotificationPermissions(completion: @escaping (Bool) -> Void)
     func requestNotificationPermissions(completion: @escaping (Bool) -> Void)
+    @MainActor func applySnooze(for duration: TimeInterval) async
 }
 }
 
 
 enum GlucoseSourceKey: String {
 enum GlucoseSourceKey: String {
@@ -40,6 +41,12 @@ protocol pumpNotificationObserver {
     func pumpRemoveNotification()
     func pumpRemoveNotification()
 }
 }
 
 
+// MARK: - SnoozeObserver Protocol
+
+protocol SnoozeObserver {
+    @MainActor func snoozeDidChange(_ untilDate: Date)
+}
+
 final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
 final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
     enum Identifier: String {
     enum Identifier: String {
         case glucoseNotification = "Trio.glucoseNotification"
         case glucoseNotification = "Trio.glucoseNotification"
@@ -61,6 +68,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     @Injected(as: FetchGlucoseManager.self) private var sourceInfoProvider: SourceInfoProvider!
     @Injected(as: FetchGlucoseManager.self) private var sourceInfoProvider: SourceInfoProvider!
 
 
     @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
     @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
+    // The glucose notification observers below (Core Data saves and the storage publisher) can fire for the same
+    // reading, so we persist the last alert token to avoid enqueueing identical high/low notifications multiple times.
+    @Persisted(key: "UserNotificationsManager.lastGlucoseAlertToken") private var lastGlucoseAlertToken: String = ""
 
 
     private let notificationCenter = UNUserNotificationCenter.current()
     private let notificationCenter = UNUserNotificationCenter.current()
     private var lifetime = Lifetime()
     private var lifetime = Lifetime()
@@ -95,11 +105,28 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         Task {
         Task {
             await sendGlucoseNotification()
             await sendGlucoseNotification()
         }
         }
+        configureNotificationCategories()
         registerHandlers()
         registerHandlers()
         registerSubscribers()
         registerSubscribers()
         subscribeOnLoop()
         subscribeOnLoop()
     }
     }
 
 
+    private func configureNotificationCategories() {
+        notificationCenter.getNotificationCategories { [weak self] existingCategories in
+            guard let self else { return }
+
+            let glucoseCategory = NotificationCategoryFactory.createGlucoseCategory()
+
+            var categories = existingCategories
+            categories.update(with: glucoseCategory)
+            // UNUserNotificationCenter methods should be called on main thread
+            Task { @MainActor [weak self] in
+                guard let self else { return }
+                self.notificationCenter.setNotificationCategories(categories)
+            }
+        }
+    }
+
     private func subscribeOnLoop() {
     private func subscribeOnLoop() {
         apsManager.lastLoopDateSubject
         apsManager.lastLoopDateSubject
             .sink { [weak self] date in
             .sink { [weak self] date in
@@ -271,6 +298,10 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                 try viewContext.existingObject(with: id) as? GlucoseStored
                 try viewContext.existingObject(with: id) as? GlucoseStored
             }
             }
 
 
+            if glucoseStorage.alarm == .none {
+                lastGlucoseAlertToken = ""
+            }
+
             guard let lastReading = glucoseObjects.first?.glucose,
             guard let lastReading = glucoseObjects.first?.glucose,
                   let secondLastReading = glucoseObjects.dropFirst().first?.glucose,
                   let secondLastReading = glucoseObjects.dropFirst().first?.glucose,
                   let lastDirection = glucoseObjects.first?.directionEnum?.symbol else { return }
                   let lastDirection = glucoseObjects.first?.directionEnum?.symbol else { return }
@@ -305,6 +336,15 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                 titles.append(String(localized: "(Snoozed)", comment: "(Snoozed)"))
                 titles.append(String(localized: "(Snoozed)", comment: "(Snoozed)"))
                 notificationAlarm = false
                 notificationAlarm = false
             } else {
             } else {
+                let token = alertToken(from: glucoseObjects.first)
+
+                if token == "unknown" {
+                    warning(.service, "Missing glucose token fields; skipping notification to avoid re-alerting")
+                    return
+                }
+                if notificationAlarm, token == lastGlucoseAlertToken {
+                    return
+                }
                 titles.append(body)
                 titles.append(body)
                 let content = UNMutableNotificationContent()
                 let content = UNMutableNotificationContent()
                 content.title = titles.joined(separator: " ")
                 content.title = titles.joined(separator: " ")
@@ -313,6 +353,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                 if notificationAlarm {
                 if notificationAlarm {
                     content.sound = .default
                     content.sound = .default
                     content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
                     content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
+                    content.categoryIdentifier = NotificationCategoryIdentifier.trioAlert.rawValue
                 }
                 }
 
 
                 addRequest(
                 addRequest(
@@ -323,6 +364,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                     messageSubtype: .glucose,
                     messageSubtype: .glucose,
                     action: NotificationAction.snooze
                     action: NotificationAction.snooze
                 )
                 )
+                if notificationAlarm {
+                    lastGlucoseAlertToken = token
+                }
             }
             }
         } catch {
         } catch {
             debugPrint(
             debugPrint(
@@ -331,6 +375,23 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
         }
     }
     }
 
 
+    private func alertToken(from glucose: GlucoseStored?) -> String {
+        if let id = glucose?.id?.uuidString { return id }
+
+        if let date = glucose?.date {
+            let roundedMinute = Int((date.timeIntervalSince1970 / 60).rounded())
+            return "date-\(roundedMinute)"
+        }
+
+        // Stable fallback for Core Data objects:
+        if let glucose, !glucose.objectID.isTemporaryID {
+            return "objectID-\(glucose.objectID.uriRepresentation().absoluteString)"
+        }
+
+        // Stable “unknown” fallback: prevents repeated alarms when identifiers are missing
+        return "unknown"
+    }
+
     private func glucoseText(glucoseValue: Int, delta: Int?, direction: String?) -> String {
     private func glucoseText(glucoseValue: Int, delta: Int?, direction: String?) -> String {
         let units = settingsManager.settings.units
         let units = settingsManager.settings.units
         let glucoseText = glucoseFormatter
         let glucoseText = glucoseFormatter
@@ -409,6 +470,19 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
         }
     }
     }
 
 
+    @MainActor func applySnooze(for duration: TimeInterval) async {
+        let untilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast
+        snoozeUntilDate = untilDate
+        lastGlucoseAlertToken = ""
+        // removeGlucoseNotifications() is safe to call here since we're @MainActor
+        removeGlucoseNotifications()
+
+        // Notify observers that snooze was applied
+        broadcaster.notify(SnoozeObserver.self, on: .main) { (observer: SnoozeObserver) in
+            observer.snoozeDidChange(untilDate)
+        }
+    }
+
     private func addRequest(
     private func addRequest(
         identifier: Identifier,
         identifier: Identifier,
         content: UNMutableNotificationContent,
         content: UNMutableNotificationContent,
@@ -571,6 +645,14 @@ extension BaseUserNotificationsManager: pumpNotificationObserver {
             self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
             self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
         }
         }
     }
     }
+
+    /// Removes all glucose notifications (delivered and pending).
+    /// Must be called from the main thread. Safe to call from @MainActor contexts.
+    @MainActor private func removeGlucoseNotifications() {
+        let identifier = Identifier.glucoseNotification.rawValue
+        notificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier])
+        notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier])
+    }
 }
 }
 
 
 extension BaseUserNotificationsManager: DeterminationObserver {
 extension BaseUserNotificationsManager: DeterminationObserver {
@@ -595,29 +677,46 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
         completionHandler([.banner, .badge, .sound, .list])
         completionHandler([.banner, .badge, .sound, .list])
     }
     }
 
 
+    /// UNUserNotificationCenterDelegate method called when user interacts with a notification.
+    /// This can be called off the main thread, so we ensure all work happens on @MainActor.
     func userNotificationCenter(
     func userNotificationCenter(
         _: UNUserNotificationCenter,
         _: UNUserNotificationCenter,
         didReceive response: UNNotificationResponse,
         didReceive response: UNNotificationResponse,
         withCompletionHandler completionHandler: @escaping () -> Void
         withCompletionHandler completionHandler: @escaping () -> Void
     ) {
     ) {
         defer { completionHandler() }
         defer { completionHandler() }
+
+        // Handle quick snooze actions (from notification action buttons)
+        if let quickAction = NotificationResponseAction(rawValue: response.actionIdentifier) {
+            Task { @MainActor [weak self] in
+                guard let self else { return }
+                await self.applySnooze(for: quickAction.duration)
+            }
+            return
+        }
+
+        // Handle other notification actions (e.g., tapping notification body)
         guard let actionRaw = response.notification.request.content.userInfo[NotificationAction.key] as? String,
         guard let actionRaw = response.notification.request.content.userInfo[NotificationAction.key] as? String,
               let action = NotificationAction(rawValue: actionRaw)
               let action = NotificationAction(rawValue: actionRaw)
         else { return }
         else { return }
 
 
-        switch action {
-        case .snooze:
-            router.mainModalScreen.send(.snooze)
-        case .pumpConfig:
-            let messageCont = MessageContent(
-                content: response.notification.request.content.body,
-                type: MessageType.other,
-                subtype: .pump,
-                useAPN: false,
-                action: .pumpConfig
-            )
-            router.alertMessage.send(messageCont)
-        default: break
+        // Ensure UI operations happen on main thread using Task for consistency
+        Task { @MainActor [weak self] in
+            guard let self = self else { return }
+            switch action {
+            case .snooze:
+                self.router.mainModalScreen.send(.snooze)
+            case .pumpConfig:
+                let messageCont = MessageContent(
+                    content: response.notification.request.content.body,
+                    type: MessageType.other,
+                    subtype: .pump,
+                    useAPN: false,
+                    action: .pumpConfig
+                )
+                self.router.alertMessage.send(messageCont)
+            default: break
+            }
         }
         }
     }
     }
 }
 }

+ 31 - 16
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -25,6 +25,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     @Injected() private var tempTargetStorage: TempTargetsStorage!
     @Injected() private var tempTargetStorage: TempTargetsStorage!
     @Injected() private var bolusCalculationManager: BolusCalculationManager!
     @Injected() private var bolusCalculationManager: BolusCalculationManager!
     @Injected() private var iobService: IOBService!
     @Injected() private var iobService: IOBService!
+    @Injected() private var notificationsManager: UserNotificationsManager!
 
 
     private var units: GlucoseUnits = .mgdL
     private var units: GlucoseUnits = .mgdL
     private var glucoseColorScheme: GlucoseColorScheme = .staticColor
     private var glucoseColorScheme: GlucoseColorScheme = .staticColor
@@ -553,16 +554,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     }
     }
 
 
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
-        DispatchQueue.main.async { [weak self] in
-            if let logs = message["watchLogs"] as? String {
-                SimpleLogReporter.appendToWatchLog(logs)
-            }
+        // Handle logs first - doesn't need self, so it can run even during teardown
+        if let logs = message["watchLogs"] as? String {
+            SimpleLogReporter.appendToWatchLog(logs)
+        }
+
+        Task { @MainActor [weak self] in
+            guard let self else { return }
 
 
             if let requestWatchUpdate = message[WatchMessageKeys.requestWatchUpdate] as? String,
             if let requestWatchUpdate = message[WatchMessageKeys.requestWatchUpdate] as? String,
                requestWatchUpdate == WatchMessageKeys.watchState
                requestWatchUpdate == WatchMessageKeys.watchState
             {
             {
                 debug(.watchManager, "📱 Watch requested watch state data update.")
                 debug(.watchManager, "📱 Watch requested watch state data update.")
-                guard let self = self else { return }
                 // Skip if no watch is paired or app not installed
                 // Skip if no watch is paired or app not installed
                 guard let session = self.session, session.isPaired, session.isReachable,
                 guard let session = self.session, session.isPaired, session.isReachable,
                       session.isWatchAppInstalled else { return }
                       session.isWatchAppInstalled else { return }
@@ -573,19 +576,23 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 return
                 return
             }
             }
 
 
-            if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
-               message[WatchMessageKeys.carbs] == nil,
-               message[WatchMessageKeys.date] == nil
+            if let snoozeMinutes = message[WatchMessageKeys.snoozeDuration] as? Int {
+                debug(.watchManager, "📱 Received snooze request from watch: \(snoozeMinutes) minutes")
+                await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60))
+                return
+            } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
+                      message[WatchMessageKeys.carbs] == nil,
+                      message[WatchMessageKeys.date] == nil
             {
             {
                 debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
                 debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
-                self?.handleBolusRequest(Decimal(bolusAmount))
+                self.handleBolusRequest(Decimal(bolusAmount))
             } else if let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
             } else if let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
                       let timestamp = message[WatchMessageKeys.date] as? TimeInterval,
                       let timestamp = message[WatchMessageKeys.date] as? TimeInterval,
                       message[WatchMessageKeys.bolus] == nil
                       message[WatchMessageKeys.bolus] == nil
             {
             {
                 let date = Date(timeIntervalSince1970: timestamp)
                 let date = Date(timeIntervalSince1970: timestamp)
                 debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
                 debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
-                self?.handleCarbsRequest(carbsAmount, date)
+                self.handleCarbsRequest(carbsAmount, date)
             } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
             } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
                       let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
                       let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
                       let timestamp = message[WatchMessageKeys.date] as? TimeInterval
                       let timestamp = message[WatchMessageKeys.date] as? TimeInterval
@@ -595,11 +602,11 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     .watchManager,
                     .watchManager,
                     "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
                     "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
                 )
                 )
-                self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
+                self.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
             } else {
             } else {
                 debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received:  \(message)")
                 debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received:  \(message)")
                 // Acknowledge failure
                 // Acknowledge failure
-                self?.sendAcknowledgment(
+                self.sendAcknowledgment(
                     toWatch: false,
                     toWatch: false,
                     message: "Error! Invalid or incomplete data received from watch.",
                     message: "Error! Invalid or incomplete data received from watch.",
                     ackCode: .genericFailure
                     ackCode: .genericFailure
@@ -608,22 +615,22 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
 
             if message[WatchMessageKeys.cancelOverride] as? Bool == true {
             if message[WatchMessageKeys.cancelOverride] as? Bool == true {
                 debug(.watchManager, "📱 Received cancel override request from watch")
                 debug(.watchManager, "📱 Received cancel override request from watch")
-                self?.handleCancelOverride()
+                self.handleCancelOverride()
             }
             }
 
 
             if let presetName = message[WatchMessageKeys.activateOverride] as? String {
             if let presetName = message[WatchMessageKeys.activateOverride] as? String {
                 debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
                 debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
-                self?.handleActivateOverride(presetName)
+                self.handleActivateOverride(presetName)
             }
             }
 
 
             if let presetName = message[WatchMessageKeys.activateTempTarget] as? String {
             if let presetName = message[WatchMessageKeys.activateTempTarget] as? String {
                 debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
                 debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
-                self?.handleActivateTempTarget(presetName)
+                self.handleActivateTempTarget(presetName)
             }
             }
 
 
             if message[WatchMessageKeys.cancelTempTarget] as? Bool == true {
             if message[WatchMessageKeys.cancelTempTarget] as? Bool == true {
                 debug(.watchManager, "📱 Received cancel temp target request from watch")
                 debug(.watchManager, "📱 Received cancel temp target request from watch")
-                self?.handleCancelTempTarget()
+                self.handleCancelTempTarget()
             }
             }
 
 
             if message[WatchMessageKeys.requestBolusRecommendation] as? Bool == true {
             if message[WatchMessageKeys.requestBolusRecommendation] as? Bool == true {
@@ -684,6 +691,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         if let logs = userInfo["watchLogs"] as? String {
         if let logs = userInfo["watchLogs"] as? String {
             SimpleLogReporter.appendToWatchLog(logs)
             SimpleLogReporter.appendToWatchLog(logs)
         }
         }
+
+        if let snoozeMinutes = userInfo[WatchMessageKeys.snoozeDuration] as? Int {
+            debug(.watchManager, "📱 Received snooze userInfo from watch: \(snoozeMinutes) minutes")
+            Task { @MainActor [weak self] in
+                guard let self else { return }
+                await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60))
+            }
+        }
     }
     }
 
 
     #if os(iOS)
     #if os(iOS)

+ 204 - 0
Trio/Sources/Services/WatchManager/FLOW_DIAGRAM.md

@@ -0,0 +1,204 @@
+# Garmin Update Flow - Visual Diagram
+
+## New Simplified Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                        Loop Cycle Completes                      │
+└───────────────────┬─────────────────────────────────────────────┘
+                    │
+                    ├─────────────────────────────┐
+                    │                             │
+                    ↓                             ↓
+        ┌──────────────────────┐      ┌──────────────────────┐
+        │  Determination       │      │  IOB Update          │
+        │  CoreData Change     │      │  iobPublisher        │
+        └──────────┬───────────┘      └──────────┬───────────┘
+                   │                             │
+                   │  .send(data)                │  .send(data)
+                   ↓                             ↓
+        ┌──────────────────────────────────────────────────────┐
+        │         determinationSubject                          │
+        │         (PassthroughSubject<Data, Never>)            │
+        └──────────────────┬───────────────────────────────────┘
+                           │
+                           │  .throttle(for: .seconds(20),
+                           │            latest: false)
+                           ↓
+        ┌──────────────────────────────────────────────────────┐
+        │              Combine Throttle Logic                   │
+        │   .throttle(for: .seconds(20), latest: false)        │
+        │                                                       │
+        │  ┌────────────────────────────────────┐             │
+        │  │ Event 1 (t=0s)    → HOLD 📦        │             │
+        │  │   [Start 20s timer]                │             │
+        │  │ Event 2 (t=0.5s)  → DROP ❌        │             │
+        │  │ Event 3 (t=1s)    → DROP ❌        │             │
+        │  │ Event 4 (t=5s)    → DROP ❌        │             │
+        │  │ [t=20s: Timer fires]               │             │
+        │  │   → SEND Event 1 ✅                │             │
+        │  │                                    │             │
+        │  │ Event 5 (t=20.1s) → HOLD 📦        │             │
+        │  │   [Start new 20s timer]            │             │
+        │  │ Event 6 (t=23s)   → DROP ❌        │             │
+        │  │ [t=40.1s: Timer fires]             │             │
+        │  │   → SEND Event 5 ✅                │             │
+        │  └────────────────────────────────────┘             │
+        │                                                       │
+        │  Pattern: HOLD first → DROP rest → SEND after 20s  │
+        └──────────────────┬───────────────────────────────────┘
+                           │
+                           ↓
+        ┌──────────────────────────────────────────────────────┐
+        │         subscribeToDeterminationThrottle()            │
+        │                                                       │
+        │  • Check if recent watchface change (<25s)           │
+        │    - If yes: Don't cache (might be old format) ⚠️    │
+        │    - If no: Cache data ✅                            │
+        │  • Convert Data → JSON                               │
+        │  • Set lastImmediateSendTime                         │
+        │  • Log: "Sending determination/IOB" (if enabled)     │
+        └──────────────────┬───────────────────────────────────┘
+                           │
+                           ↓
+        ┌──────────────────────────────────────────────────────┐
+        │         broadcastStateToWatchApps()                   │
+        │                                                       │
+        │  ├─> Watchface App (5A643C13...)                     │
+        │  └─> Data Field App (71CF0982...)                    │
+        └───────────────────────────────────────────────────────┘
+```
+
+## Other Update Sources (Unchanged)
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                    Glucose Update (Stale Loop)                   │
+│                    (Loop age > 8 minutes)                        │
+└───────────────────┬─────────────────────────────────────────────┘
+                    │
+                    │  Immediate send - no throttle
+                    ↓
+        ┌──────────────────────────────────────────────────────┐
+        │         sendWatchStateDataImmediately()               │
+        │                                                       │
+        │  • Convert Data → JSON                               │
+        │  • Set lastImmediateSendTime                         │
+        │  • broadcastStateToWatchApps()                       │
+        └───────────────────────────────────────────────────────┘
+
+
+┌─────────────────────────────────────────────────────────────────┐
+│              Status Request / Settings Changes                   │
+└───────────────────┬─────────────────────────────────────────────┘
+                    │
+                    │  30s throttle
+                    ↓
+        ┌──────────────────────────────────────────────────────┐
+        │         sendWatchStateDataWith30sThrottle()           │
+        │                                                       │
+        │  • Store pending data                                │
+        │  • Start/update 30s timer                            │
+        │  • Check lastImmediateSendTime before firing         │
+        │  • broadcastStateToWatchApps() after 30s             │
+        └───────────────────────────────────────────────────────┘
+```
+
+## Comparison: Old vs New
+
+### Old Architecture (Complex)
+```
+Determination ──> sendWatchStateDataImmediately() ──> Watch
+                      │
+                      └─> Set lastImmediateSendTime
+                      
+IOB ──> sendWatchStateDataWith30sThrottle() ──> Watch
+         │
+         └─> Check lastImmediateSendTime? ❌ Race condition!
+         └─> Start 30s timer
+         └─> Cancel if determination fired? ⚠️ Complex!
+```
+
+### New Architecture (Simple)
+```
+Determination ──┐
+                ├──> determinationSubject ──> .throttle(10s) ──> Watch
+IOB ───────────┘
+```
+
+## Timeline Example
+
+```
+Time    Event                          Action
+──────────────────────────────────────────────────────────────────
+0:00    Loop completes                 
+        ├─ Determination fires ─┐
+        └─ IOB fires ───────────┴──> determinationSubject.send()
+                                                │
+0:00                                   Throttle: SEND ✅
+                                       Log: "Sending determination/IOB"
+                                                │
+0:00-10s Multiple loop cycles         Throttle: DROP ALL ❌
+        (rapid determinations/IOB)              │
+                                                │
+10:01   Next loop completes            Throttle: SEND ✅
+        ├─ Determination fires ─┐      Log: "Sending determination/IOB"
+        └─ IOB fires ───────────┘
+                                                │
+15:00   Status request arrives         30s timer starts
+                                       (separate pipeline)
+                                                │
+20:01   Loop completes                 Throttle: SEND ✅
+        ├─ Determination fires ─┐      (30s timer cancelled - recent send)
+        └─ IOB fires ───────────┘
+```
+
+## Key Architectural Decisions
+
+### Why Combine Throttle Instead of Manual Timer?
+
+**Combine throttle:**
+✅ Built-in deduplication
+✅ Thread-safe by design
+✅ Predictable scheduler behavior
+✅ Less code to maintain
+✅ No race conditions
+
+**Manual timer:**
+❌ Complex lifecycle management
+❌ Race conditions between publishers
+❌ More code to test
+❌ Threading concerns
+❌ Easy to introduce bugs
+
+### Why 10 Seconds?
+
+1. **Loop cycle timing:** Typical loop = 5 minutes
+2. **Multiple events = same cycle:** Events within 10s are from same loop
+3. **Responsiveness:** 10s is imperceptible to users
+4. **Battery efficiency:** Reduces watch transmissions by ~80%
+
+### Why `latest: false`?
+
+| Setting | Behavior | Result |
+|---------|----------|--------|
+| `latest: false` | Keep **first** event, drop rest | Send immediately when loop completes ✅ |
+| `latest: true` | Drop events, send **last** one after throttle | 10 second delay every time ❌ |
+
+We want immediate response when data arrives, not delayed response.
+
+## Code Metrics
+
+### Lines of Code
+- **Old approach:** ~150 lines of throttling logic
+- **New approach:** ~60 lines of throttling logic
+- **Reduction:** 60% less code
+
+### Complexity
+- **Old approach:** 3 throttle mechanisms (immediate, 10s manual, 30s manual)
+- **New approach:** 2 throttle mechanisms (10s Combine, 30s manual)
+- **Timer objects:** Reduced from 2 to 1
+
+### Edge Cases Handled
+- **Old approach:** ~8 edge cases (race conditions, timer coordination, etc.)
+- **New approach:** ~3 edge cases (all handled by Combine)

Dosya farkı çok büyük olduğundan ihmal edildi
+ 728 - 284
Trio/Sources/Services/WatchManager/GarminManager.swift


+ 0 - 2
Trio/Sources/Views/SettingInputSection.swift

@@ -99,8 +99,6 @@ struct SettingInputSection<VerboseHint: View>: View {
             return pickerSettingsProvider.settings.individualAdjustmentFactor
             return pickerSettingsProvider.settings.individualAdjustmentFactor
         case "delay":
         case "delay":
             return pickerSettingsProvider.settings.delay
             return pickerSettingsProvider.settings.delay
-        case "timeCap":
-            return pickerSettingsProvider.settings.timeCap
         case "minuteInterval":
         case "minuteInterval":
             return pickerSettingsProvider.settings.minuteInterval
             return pickerSettingsProvider.settings.minuteInterval
         case "high":
         case "high":

+ 234 - 0
TrioTests/CoreDataTests/CarbsStorageTests.swift

@@ -123,6 +123,240 @@ import Testing
         #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
         #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
     }
     }
 
 
+    @Test(
+        "Store carb entry with fat/protein creates capped, spaced FPU entries (defaults: adjustment=0.5, delay=60m)"
+    ) func testStoreFatProteinCarbEntryCreatesFPUEntries() async throws {
+        let fpuID = UUID().uuidString
+        let baseDate = Date(timeIntervalSince1970: 1_700_000_000)
+
+        // Defaults:
+        // adjustment = 0.5, delay = 60
+        //
+        // fat=50g -> 450 kcal
+        // protein=100g -> 400 kcal
+        // kcal total = 850
+        // (kcal/10) = 85
+        // 85 * 0.5 = 42.5
+        // Int(42.5) = 42 equivalents -> two FPU entries: 21g each
+        let mealEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: baseDate,
+            actualDate: baseDate,
+            carbs: 30,
+            fat: 50,
+            protein: 100,
+            note: "FPU deterministic default split test",
+            enteredBy: "Test",
+            isFPU: false,
+            fpuID: fpuID
+        )
+
+        try await storage.storeCarbs([mealEntry], areFetchedFromRemote: false)
+
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "fpuID == %@", fpuID),
+            key: "date",
+            ascending: true
+        ) as? [CarbEntryStored]
+
+        guard let storedEntries else {
+            throw TestError("Failed to fetch entries for fpuID")
+        }
+
+        #expect(!storedEntries.isEmpty, "Should have stored entries")
+
+        let originalCarbEntry = storedEntries.first(where: { $0.isFPU == false })
+        #expect(originalCarbEntry != nil, "Should have one non-FPU original entry")
+        #expect(originalCarbEntry?.carbs == 30, "Original carbs should match")
+        #expect(originalCarbEntry?.fat == 50, "Original fat should match")
+        #expect(originalCarbEntry?.protein == 100, "Original protein should match")
+
+        let fpuEntries = storedEntries.filter { $0.isFPU == true }
+        #expect(fpuEntries.count == 2, "Expected exactly one FPU entry under default settings")
+        #expect(Int(fpuEntries[0].carbs) == 21, "Expected 20g carb equivalents under default settings")
+
+        for fpuEntry in fpuEntries {
+            #expect(fpuEntry.fat == 0, "FPU fat must be 0")
+            #expect(fpuEntry.protein == 0, "FPU protein must be 0")
+            #expect(fpuEntry.carbs >= 10, "FPU carbs must be >= 10g")
+            #expect(fpuEntry.carbs <= 33, "FPU carbs must be <= 33g")
+            #expect(Double(fpuEntry.carbs).truncatingRemainder(dividingBy: 1) == 0, "FPU carbs must be whole grams")
+        }
+
+        let scheduledTotal = fpuEntries.reduce(0) { partialResult, fpuEntry in
+            partialResult + Int(fpuEntry.carbs)
+        }
+        #expect(scheduledTotal <= 99, "Scheduled FPU carbs must be capped at 99g")
+
+        // Timing: stable assertions
+        // - first FPU entry must be at least +60m after the *input* timestamp (createdAt/actualDate),
+        //   but storage may choose a different internal baseDate, so don't assert exact equality.
+        let fpuDates = fpuEntries.compactMap(\.date).sorted()
+        #expect(fpuDates.count == 2, "FPU entry should have a date")
+
+        let firstFpuDate = fpuDates[0]
+        #expect(
+            firstFpuDate >= baseDate.addingTimeInterval(60 * 60),
+            "First FPU entry should not be scheduled earlier than +60 minutes after the input timestamp"
+        )
+
+        #expect(
+            storedEntries.allSatisfy { $0.fpuID?.uuidString == fpuID },
+            "All entries should share the same fpuID"
+        )
+    }
+
+    @Test(
+        "Store very large fat/protein meal caps FPU equivalents at 99g and splits into 3×33g (defaults: adjustment=0.5, delay=60m)"
+    ) func testStoreVeryLargeFatProteinMealCapsAndSplits() async throws {
+        let fpuID = UUID().uuidString
+        let baseDate = Date(timeIntervalSince1970: 1_700_001_000)
+
+        // Defaults:
+        // adjustment = 0.5, delay = 60
+        //
+        // fat=200g -> 1800 kcal
+        // protein=200g -> 800 kcal
+        // kcal total = 2600
+        // (kcal/10) = 260
+        // 260 * 0.5 = 130
+        // Int(130) = 130 -> capped to 99 -> split into [33, 33, 33]
+        let heftyMealEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: baseDate,
+            actualDate: baseDate,
+            carbs: 30,
+            fat: 200,
+            protein: 200,
+            note: "Hefty BBQ meal - cap test",
+            enteredBy: "Test",
+            isFPU: false,
+            fpuID: fpuID
+        )
+
+        try await storage.storeCarbs([heftyMealEntry], areFetchedFromRemote: false)
+
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "fpuID == %@", fpuID),
+            key: "date",
+            ascending: true
+        ) as? [CarbEntryStored]
+
+        guard let storedEntries else {
+            throw TestError("Failed to fetch entries for fpuID")
+        }
+
+        #expect(!storedEntries.isEmpty, "Should have stored entries")
+
+        let originalCarbEntry = storedEntries.first(where: { $0.isFPU == false })
+        #expect(originalCarbEntry != nil, "Should have one non-FPU original entry")
+        #expect(originalCarbEntry?.carbs == 30, "Original carbs should match")
+        #expect(originalCarbEntry?.fat == 200, "Original fat should match")
+        #expect(originalCarbEntry?.protein == 200, "Original protein should match")
+
+        let fpuEntries = storedEntries.filter { $0.isFPU == true }
+        #expect(fpuEntries.count == 3, "Capped large meal should create exactly 3 FPU entries")
+
+        let fpuGrams = fpuEntries.map { Int($0.carbs) }
+        #expect(fpuGrams == [33, 33, 33], "Expected capped split to be [33, 33, 33]")
+
+        let scheduledTotal = fpuEntries.reduce(0) { partialResult, fpuEntry in
+            partialResult + Int(fpuEntry.carbs)
+        }
+        #expect(scheduledTotal == 99, "Total scheduled FPU grams should be exactly 99g after cap")
+
+        for fpuEntry in fpuEntries {
+            #expect(fpuEntry.fat == 0, "FPU entry fat must be 0")
+            #expect(fpuEntry.protein == 0, "FPU entry protein must be 0")
+            #expect(fpuEntry.carbs >= 10, "FPU entry carbs must be >= 10g")
+            #expect(fpuEntry.carbs <= 33, "FPU entry carbs must be <= 33g")
+            #expect(Double(fpuEntry.carbs).truncatingRemainder(dividingBy: 1) == 0, "FPU carbs must be whole grams")
+        }
+
+        // Timing: stable assertions
+        let fpuDates = fpuEntries.compactMap(\.date).sorted()
+        #expect(fpuDates.count == 3, "All FPU entries should have a date")
+
+        let firstFpuDate = fpuDates[0]
+        #expect(
+            firstFpuDate >= baseDate.addingTimeInterval(60 * 60),
+            "First FPU entry should not be scheduled earlier than +60 minutes after the input timestamp"
+        )
+
+        for index in 1 ..< fpuDates.count {
+            let spacingSeconds = fpuDates[index].timeIntervalSince(fpuDates[index - 1])
+            #expect(Int(spacingSeconds) == 30 * 60, "FPU entries should be spaced +30 minutes apart")
+        }
+
+        #expect(
+            storedEntries.allSatisfy { $0.fpuID?.uuidString == fpuID },
+            "All entries should share the same fpuID"
+        )
+    }
+
+    @Test(
+        "Store small fat/protein meal drops FPU equivalents when total would be <10g (defaults: adjustment=0.5, delay=60m)"
+    ) func testStoreSmallFatProteinMealDropsFPUBelowMinimum() async throws {
+        let fpuID = UUID().uuidString
+        let baseDate = Date(timeIntervalSince1970: 1_700_002_000)
+
+        // Defaults:
+        // adjustment = 0.5
+        //
+        // fat=2g -> 18 kcal
+        // protein=2g -> 8 kcal
+        // kcal total = 26
+        // (kcal/10) = 2.6
+        // 2.6 * 0.5 = 1.3
+        // Int(1.3) = 1 (<10) -> should be dropped (no FPU entries)
+        let smallMealEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: baseDate,
+            actualDate: baseDate,
+            carbs: 30,
+            fat: 2,
+            protein: 2,
+            note: "Tiny macros - min threshold test",
+            enteredBy: "Test",
+            isFPU: false,
+            fpuID: fpuID
+        )
+
+        try await storage.storeCarbs([smallMealEntry], areFetchedFromRemote: false)
+
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "fpuID == %@", fpuID),
+            key: "date",
+            ascending: true
+        ) as? [CarbEntryStored]
+
+        guard let storedEntries else {
+            throw TestError("Failed to fetch entries for fpuID")
+        }
+
+        #expect(!storedEntries.isEmpty, "Should have stored at least the original entry")
+
+        let originalCarbEntry = storedEntries.first(where: { $0.isFPU == false })
+        #expect(originalCarbEntry != nil, "Should have one non-FPU original entry")
+        #expect(originalCarbEntry?.carbs == 30, "Original carbs should match")
+        #expect(originalCarbEntry?.fat == 2, "Original fat should match")
+        #expect(originalCarbEntry?.protein == 2, "Original protein should match")
+
+        let fpuEntries = storedEntries.filter { $0.isFPU == true }
+        #expect(fpuEntries.isEmpty == true, "No FPU entries should be created when equivalents are <10g")
+
+        #expect(
+            storedEntries.allSatisfy { $0.fpuID?.uuidString == fpuID },
+            "All entries should share the same fpuID"
+        )
+    }
+
     @Test("Get carbs not yet uploaded to Nightscout") func testGetCarbsNotYetUploadedToNightscout() async throws {
     @Test("Get carbs not yet uploaded to Nightscout") func testGetCarbsNotYetUploadedToNightscout() async throws {
         // Given
         // Given
         let testEntry = CarbsEntry(
         let testEntry = CarbsEntry(

+ 333 - 0
TrioTests/GlucoseSmoothingTests.swift

@@ -0,0 +1,333 @@
+import CoreData
+import Foundation
+import LoopKitUI
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("Glucose Smoothing Tests", .serialized) struct GlucoseSmoothingTests: Injectable {
+    let resolver: Resolver
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
+    var fetchGlucoseManager: BaseFetchGlucoseManager!
+    var openAPS: OpenAPS!
+
+    init() async throws {
+        coreDataStack = try await CoreDataStack.createForTests()
+        testContext = coreDataStack.newTaskContext()
+
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext)
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+
+        fetchGlucoseManager = resolver.resolve(FetchGlucoseManager.self)! as? BaseFetchGlucoseManager
+
+        let fileStorage = resolver.resolve(FileStorage.self)!
+        openAPS = OpenAPS(storage: fileStorage, tddStorage: MockTDDStorage())
+    }
+
+    // MARK: - Exponential Smoothing Tests
+
+    @Test(
+        "Exponential smoothing writes smoothed glucose for CGM values when enough data exists"
+    ) func testExponentialSmoothingStoresSmoothedValues() async throws {
+        let glucoseValues: [Int16] = [100, 105, 110, 115, 120, 125]
+        await createGlucoseSequence(values: glucoseValues, interval: 5 * 60, isManual: false)
+
+        await fetchGlucoseManager.exponentialSmoothingGlucose(context: testContext)
+
+        let fetchedAscending = try await fetchAndSortGlucose()
+
+        // We expect at least the most recent few values to get smoothed values written.
+        // The Kotlin/port writes to data[i] for i in 0..<limit, where data is newest-first.
+        // With 6 values:
+        // - recordCount = 6
+        // - validWindowCount starts at 5, no gap => remains 5
+        // - smoothing produces blended.count == 5
+        // - apply limit = min(5, 6) = 5 => most recent 5 entries get smoothedGlucose
+        //
+        // In ascending order, "most recent 5" are indices 1...5. Oldest (index 0) is not guaranteed to be updated.
+        #expect(fetchedAscending.count == 6)
+
+        let smoothedValues = fetchedAscending.compactMap { $0.smoothedGlucose?.decimalValue }
+        #expect(smoothedValues.count >= 5, "Expected at least 5 smoothed values to be stored.")
+
+        for (i, value) in smoothedValues.enumerated() {
+            #expect(value >= 39, "Smoothed glucose at index \(i) should be clamped to at least 39, got \(value).")
+            #expect(
+                value == value.rounded(toPlaces: 0),
+                "Smoothed glucose at index \(i) should be rounded to an integer, got \(value)."
+            )
+        }
+    }
+
+    @Test("Exponential smoothing does not smooth manual glucose entries") func testExponentialSmoothingIgnoresManual() async throws {
+        // GIVEN: Mixed manual + CGM values
+        await createGlucoseSequence(values: [100, 105, 110, 115, 120].map(Int16.init), interval: 5 * 60, isManual: false)
+        await createGlucose(glucose: 130, smoothed: nil, isManual: true, date: Date().addingTimeInterval(6 * 5 * 60))
+
+        // WHEN
+        await fetchGlucoseManager.exponentialSmoothingGlucose(context: testContext)
+
+        // THEN
+        let allAscending = try await fetchAndSortGlucose()
+        let manual = allAscending.first(where: { $0.isManual })
+
+        #expect(manual != nil, "Expected a manual glucose entry.")
+        #expect(manual?.smoothedGlucose == nil, "Manual entries must not be smoothed/stored.")
+    }
+
+    @Test(
+        "Exponential smoothing clamps smoothed glucose to >= 39 and rounds to integer"
+    ) func testExponentialSmoothingClampAndRounding() async throws {
+        // GIVEN
+        let glucoseValues: [Int16] = [40, 39, 41, 42, 43, 44]
+        await createGlucoseSequence(values: glucoseValues, interval: 5 * 60, isManual: false)
+
+        // WHEN
+        await fetchGlucoseManager.exponentialSmoothingGlucose(context: testContext)
+
+        // THEN
+        let fetchedAscending = try await fetchAndSortGlucose()
+
+        let smoothedValues = fetchedAscending
+            .compactMap { $0.smoothedGlucose?.decimalValue }
+            .filter { $0 > 0 }
+
+        #expect(!smoothedValues.isEmpty, "Expected at least one smoothed glucose value to be stored.")
+
+        for (index, smoothed) in smoothedValues.enumerated() {
+            #expect(
+                smoothed >= 39,
+                "Smoothed glucose must be clamped to >= 39, got \(smoothed) at index \(index)."
+            )
+
+            #expect(
+                smoothed == smoothed.rounded(toPlaces: 0),
+                "Smoothed glucose must be an integer value, got \(smoothed) at index \(index)."
+            )
+        }
+    }
+
+    @Test(
+        "Exponential smoothing stops at gaps >= 12 minutes and only updates the most recent window"
+    ) func testExponentialSmoothingGapStopsWindow() async throws {
+        let now = Date()
+
+        var dates: [Date] = []
+        var values: [Int16] = []
+
+        // Older contiguous block (should remain untouched)
+        for i in 0 ..< 10 {
+            dates.append(now.addingTimeInterval(Double(i) * 5 * 60))
+            values.append(Int16(100 + i * 5))
+        }
+
+        // GAP (15 minutes)
+        let gapStart = now.addingTimeInterval(Double(10) * 5 * 60 + 15 * 60)
+
+        // Recent block (too small -> fallback applies only here)
+        for i in 0 ..< 3 {
+            dates.append(gapStart.addingTimeInterval(Double(i) * 5 * 60))
+            values.append(Int16(200 + i * 5))
+        }
+
+        await createGlucoseSequence(values: values, dates: dates, isManual: false)
+
+        await fetchGlucoseManager.exponentialSmoothingGlucose(context: testContext)
+
+        let ascending = try await fetchAndSortGlucose()
+        #expect(ascending.count == values.count)
+
+        // Split into:
+        // - older block (before gap)
+        // - recent block (after gap)
+        let olderBlock = ascending.prefix(10)
+        let recentBlock = ascending.suffix(3)
+
+        // --- ASSERT 1: Older values should NOT be overwritten ---
+        for (index, obj) in olderBlock.enumerated() {
+            #expect(
+                obj.smoothedGlucose == nil,
+                "Older value at index \(index) should remain untouched (no fallback overwrite)."
+            )
+        }
+
+        // --- ASSERT 2: Recent values should be filled by fallback ---
+        for (index, obj) in recentBlock.enumerated() {
+            guard let smoothed = obj.smoothedGlucose?.decimalValue else {
+                #expect(false, "Recent value at index \(index) should have smoothedGlucose set.")
+                continue
+            }
+
+            #expect(
+                smoothed >= 39,
+                "Fallback smoothed glucose must be clamped to >= 39, got \(smoothed)."
+            )
+
+            #expect(
+                smoothed == smoothed.rounded(toPlaces: 0),
+                "Fallback smoothed glucose must be rounded to integer, got \(smoothed)."
+            )
+        }
+    }
+
+    @Test(
+        "Exponential smoothing treats 38 mg/dL as xDrip error and clamps stored smoothed glucose"
+    ) func testExponentialSmoothingXDrip38StopsWindow() async throws {
+        // GIVEN
+        let values: [Int16] = [100, 105, 110, 38, 120, 125]
+        await createGlucoseSequence(values: values, interval: 5 * 60, isManual: false)
+
+        // WHEN
+        await fetchGlucoseManager.exponentialSmoothingGlucose(context: testContext)
+
+        // THEN
+        let ascending = try await fetchAndSortGlucose()
+        #expect(ascending.count == 6)
+
+        let smoothedValues = ascending
+            .compactMap { $0.smoothedGlucose?.decimalValue }
+            .filter { $0 > 0 }
+
+        #expect(
+            !smoothedValues.isEmpty,
+            "Expected at least one smoothed glucose value to be stored."
+        )
+
+        for (index, smoothed) in smoothedValues.enumerated() {
+            #expect(
+                smoothed >= 39,
+                "Smoothed glucose must be clamped to >= 39 even around xDrip 38, got \(smoothed) at index \(index)."
+            )
+            #expect(
+                smoothed == smoothed.rounded(toPlaces: 0),
+                "Smoothed glucose must be rounded to an integer, got \(smoothed) at index \(index)."
+            )
+        }
+    }
+
+    // MARK: - OpenAPS Glucose Selection Tests
+
+    @Test("Algorithm uses smoothed glucose when enabled") func testAlgorithmUsesSmoothedGlucose() async throws {
+        await createGlucose(glucose: 150, smoothed: 140, isManual: false, date: Date())
+
+        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: true)
+
+        #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
+        #expect(
+            algorithmInput.first?.glucose == 140,
+            "Algorithm should have used the smoothed glucose value (140), but used \(algorithmInput.first?.glucose ?? 0)."
+        )
+    }
+
+    @Test("Algorithm uses raw glucose when smoothing is disabled") func testAlgorithmUsesRawGlucose() async throws {
+        await createGlucose(glucose: 150, smoothed: 140, isManual: false, date: Date())
+
+        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: false)
+
+        #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
+        #expect(
+            algorithmInput.first?.glucose == 150,
+            "Algorithm should have used the raw glucose value (150), but used \(algorithmInput.first?.glucose ?? 0)."
+        )
+    }
+
+    @Test("Algorithm falls back to raw glucose if smoothed value is missing") func testAlgorithmFallbackToRawGlucose() async throws {
+        await createGlucose(glucose: 150, smoothed: nil, isManual: false, date: Date())
+
+        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: true)
+
+        #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
+        #expect(
+            algorithmInput.first?.glucose == 150,
+            "Algorithm should have fallen back to the raw glucose value (150), but used \(algorithmInput.first?.glucose ?? 0)."
+        )
+    }
+
+    @Test("Algorithm ignores smoothed value for manual glucose entries") func testAlgorithmIgnoresSmoothedManualGlucose() async throws {
+        await createGlucose(glucose: 150, smoothed: 140, isManual: true, date: Date())
+
+        let algorithmInput = try await runFetchAndProcessGlucose(smoothGlucose: true)
+
+        #expect(algorithmInput.count == 1, "Expected to process one glucose entry.")
+        #expect(
+            algorithmInput.first?.glucose == 150,
+            "Algorithm should have ignored smoothing for a manual entry and used the raw value (150), but used \(algorithmInput.first?.glucose ?? 0)."
+        )
+    }
+
+    // MARK: - Helpers
+
+    private func runFetchAndProcessGlucose(smoothGlucose: Bool) async throws -> [AlgorithmGlucose] {
+        let jsonString = try await openAPS.fetchAndProcessGlucose(
+            context: testContext,
+            shouldSmoothGlucose: smoothGlucose,
+            fetchLimit: 10
+        )
+
+        let data = jsonString.data(using: .utf8)!
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .custom { decoder in
+            let container = try decoder.singleValueContainer()
+            let dateDouble = try container.decode(Double.self)
+            return Date(timeIntervalSince1970: dateDouble / 1000)
+        }
+
+        return try decoder.decode([AlgorithmGlucose].self, from: data)
+    }
+
+    private func createGlucose(glucose: Int16, smoothed: Decimal?, isManual: Bool, date: Date) async {
+        await testContext.perform {
+            let object = GlucoseStored(context: self.testContext)
+            object.date = date
+            object.glucose = glucose
+            object.smoothedGlucose = smoothed as NSDecimalNumber?
+            object.isManual = isManual
+            object.id = UUID()
+            try! self.testContext.save()
+        }
+    }
+
+    private func createGlucoseSequence(values: [Int16], dates: [Date], isManual: Bool) async {
+        precondition(values.count == dates.count)
+
+        await testContext.perform {
+            for (i, value) in values.enumerated() {
+                let object = GlucoseStored(context: self.testContext)
+                object.date = dates[i]
+                object.glucose = value
+                object.smoothedGlucose = nil
+                object.isManual = isManual
+                object.id = UUID()
+            }
+            try! self.testContext.save()
+        }
+    }
+
+    private func createGlucoseSequence(values: [Int16], interval: TimeInterval, isManual: Bool) async {
+        let now = Date()
+        let dates = values.indices.map { now.addingTimeInterval(Double($0) * interval) }
+        await createGlucoseSequence(values: values, dates: dates, isManual: isManual)
+    }
+
+    private func fetchAndSortGlucose() async throws -> [GlucoseStored] {
+        try await coreDataStack.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: .all,
+            key: "date",
+            ascending: true
+        ) as? [GlucoseStored] ?? []
+    }
+}

+ 2 - 2
TrioTests/JSONImporterTests.swift

@@ -92,7 +92,7 @@ class BundleReference {}
         ) as? [PumpEventStored] ?? []
         ) as? [PumpEventStored] ?? []
 
 
         let objectIds = allReadings.map(\.objectID)
         let objectIds = allReadings.map(\.objectID)
-        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, from: context)
+        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, orphanedResumes: [], from: context)
 
 
         var bolusTotal = 0.0
         var bolusTotal = 0.0
         var bolusCount = 0
         var bolusCount = 0
@@ -172,7 +172,7 @@ class BundleReference {}
         ) as? [PumpEventStored] ?? []
         ) as? [PumpEventStored] ?? []
 
 
         let objectIds = allReadings.map(\.objectID)
         let objectIds = allReadings.map(\.objectID)
-        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, from: context)
+        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, orphanedResumes: [], from: context)
 
 
         #expect(parsedHistory.count == 1)
         #expect(parsedHistory.count == 1)
 
 

+ 15 - 0
TrioTests/Mocks/MockTDDStorage.swift

@@ -0,0 +1,15 @@
+import LoopKitUI
+@testable import Trio
+
+struct MockTDDStorage: TDDStorage {
+    func calculateTDD(
+        pumpManager _: any LoopKitUI.PumpManagerUI,
+        pumpHistory _: [Trio.PumpHistoryEvent],
+        basalProfile _: [Trio.BasalProfileEntry]
+    ) async throws -> Trio.TDDResult {
+        TDDResult(total: 0, bolus: 0, tempBasal: 0, scheduledBasal: 0, weightedAverage: 0, hoursOfData: 0)
+    }
+
+    func storeTDD(_: Trio.TDDResult) async { /* skip */ }
+    func hasSufficientTDD() async throws -> Bool { true }
+}

+ 2 - 1
scripts/swiftformat.sh

@@ -111,4 +111,5 @@ trailingClosures \
   OmniBLE, \
   OmniBLE, \
   MinimedKit, \
   MinimedKit, \
   TidepoolService, \
   TidepoolService, \
-  DanaKit 
+  DanaKit, \
+  MedtrumKit