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

Merge branch 'dev' into fix-renewTT

Deniz Cengiz 1 год назад
Родитель
Сommit
dd4e486ddd
67 измененных файлов с 1225 добавлено и 603 удалено
  1. 1 1
      DanaKit
  2. 12 2
      Model/CoreDataStack.swift
  3. 14 0
      Model/Helper/AdjustmentStored+Helper.swift
  4. 3 0
      Model/Helper/CoreDataError.swift
  5. 0 10
      Model/Helper/OverrideStored+helper.swift
  6. 87 3
      Model/Helper/PumpEvent+helper.swift
  7. 1 1
      OmniBLE
  8. 1 1
      OmniKit
  9. 11 1
      Trio Watch App Extension/Helper/Helper+ButtonStyles.swift
  10. 20 0
      Trio Watch App Extension/Views/BolusProgressOverlay.swift
  11. 5 0
      Trio Watch App Extension/Views/GlucoseChartView.swift
  12. 13 4
      Trio Watch App Extension/Views/TrioMainWatchView.swift
  13. 14 0
      Trio Watch App Extension/WatchState.swift
  14. 27 15
      Trio.xcodeproj/project.pbxproj
  15. 9 3
      Trio/Sources/APS/APSManager.swift
  16. 5 5
      Trio/Sources/APS/CGM/CGMType.swift
  17. 1 2
      Trio/Sources/APS/CGM/PluginSource.swift
  18. 0 8
      Trio/Sources/APS/DeviceDataManager.swift
  19. 1 0
      Trio/Sources/APS/FetchGlucoseManager.swift
  20. 25 10
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  21. 1 1
      Trio/Sources/APS/Storage/OverrideStorage.swift
  22. 4 2
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  23. 3 3
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  24. 24 9
      Trio/Sources/Application/TrioApp.swift
  25. 15 0
      Trio/Sources/Helpers/CGMOptions.swift
  26. 5 1
      Trio/Sources/Logger/Logger.swift
  27. 2 0
      Trio/Sources/Models/WatchMessageKeys.swift
  28. 9 3
      Trio/Sources/Models/WatchState.swift
  29. 3 5
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  30. 1 1
      Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  31. 0 5
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  32. 4 0
      Trio/Sources/Modules/Base/BaseStateModel.swift
  33. 0 9
      Trio/Sources/Modules/CGM/CGMDataFlow.swift
  34. 0 5
      Trio/Sources/Modules/CGM/CGMProvider.swift
  35. 0 266
      Trio/Sources/Modules/CGM/View/CGMRootView.swift
  36. 5 0
      Trio/Sources/Modules/CGMSettings/CGMSettingsDataFlow.swift
  37. 5 0
      Trio/Sources/Modules/CGMSettings/CGMSettingsProvider.swift
  38. 69 65
      Trio/Sources/Modules/CGM/CGMStateModel.swift
  39. 206 0
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  40. 1 1
      Trio/Sources/Modules/CGM/View/CGMSettingsView.swift
  41. 2 2
      Trio/Sources/Modules/CGM/View/CGMSetupView.swift
  42. 210 0
      Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift
  43. 26 15
      Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  44. 3 1
      Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  45. 0 5
      Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  46. 13 2
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  47. 7 1
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  48. 2 0
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  49. 1 1
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  50. 7 3
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  51. 15 4
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  52. 1 1
      Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  53. 166 28
      Trio/Sources/Modules/Home/HomeStateModel.swift
  54. 18 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift
  55. 2 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/ForecastView.swift
  56. 2 1
      Trio/Sources/Modules/Home/View/Chart/MainChartView.swift
  57. 0 17
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  58. 100 12
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  59. 0 5
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  60. 0 15
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift
  61. 4 3
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  62. 0 5
      Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  63. 10 31
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  64. 1 4
      Trio/Sources/Router/Screen.swift
  65. 0 3
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  66. 27 0
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  67. 1 0
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit b07f236677b205d31d7ecf6144970348e8d5a3fe
+Subproject commit b44c5df260a8b38d6fd0b5851cc3aac5da5d9d57

+ 12 - 2
Model/CoreDataStack.swift

@@ -105,7 +105,7 @@ class CoreDataStack: ObservableObject {
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "persistentHistoryContext"
         taskContext.name = "persistentHistoryContext"
-//        debugPrint("Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
+        //        debugPrint("Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
 
 
         try await taskContext.perform {
         try await taskContext.perform {
             // Execute the persistent history change since the last transaction
             // Execute the persistent history change since the last transaction
@@ -120,7 +120,7 @@ class CoreDataStack: ObservableObject {
     }
     }
 
 
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
-//        debugPrint("Received \(history.count) persistent history transactions")
+        //        debugPrint("Received \(history.count) persistent history transactions")
         // Update view context with objectIDs from history change request
         // Update view context with objectIDs from history change request
         /// - Tag: mergeChanges
         /// - Tag: mergeChanges
         let viewContext = persistentContainer.viewContext
         let viewContext = persistentContainer.viewContext
@@ -150,6 +150,16 @@ class CoreDataStack: ObservableObject {
             }
             }
         }
         }
     }
     }
+
+    func initializeStack() throws {
+        // Force initialization of persistent container
+        let container = persistentContainer
+
+        // Verify the store is loaded and available
+        guard container.persistentStoreCoordinator.persistentStores.isNotEmpty else {
+            throw CoreDataError.storeNotInitializedError
+        }
+    }
 }
 }
 
 
 // MARK: - Delete
 // MARK: - Delete

+ 14 - 0
Model/Helper/AdjustmentStored+Helper.swift

@@ -0,0 +1,14 @@
+import CoreData
+import Foundation
+
+extension NSPredicate {
+    static var lastActiveAdjustmentNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND enabled == %@ AND isUploadedToNS == %@",
+            date as NSDate,
+            true as NSNumber,
+            false as NSNumber
+        )
+    }
+}

+ 3 - 0
Model/Helper/CoreDataError.swift

@@ -7,6 +7,7 @@ enum CoreDataError: Error {
     case persistentHistoryChangeError
     case persistentHistoryChangeError
     case unexpectedError(error: Error)
     case unexpectedError(error: Error)
     case fetchError
     case fetchError
+    case storeNotInitializedError
 }
 }
 
 
 extension CoreDataError: LocalizedError {
 extension CoreDataError: LocalizedError {
@@ -24,6 +25,8 @@ extension CoreDataError: LocalizedError {
             return NSLocalizedString("Received unexpected error. \(error.localizedDescription)", comment: "")
             return NSLocalizedString("Received unexpected error. \(error.localizedDescription)", comment: "")
         case .fetchError:
         case .fetchError:
             return NSLocalizedString("Failed to fetch object \(DebuggingIdentifiers.failed).", comment: "")
             return NSLocalizedString("Failed to fetch object \(DebuggingIdentifiers.failed).", comment: "")
+        case .storeNotInitializedError:
+            return NSLocalizedString("Failed to initialize Core Data's persistent store.", comment: "")
         }
         }
     }
     }
 }
 }

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

@@ -14,16 +14,6 @@ extension NSPredicate {
             true as NSNumber
             true as NSNumber
         )
         )
     }
     }
-
-    static var lastActiveOverrideNotYetUploadedToNightscout: NSPredicate {
-        let date = Date.oneDayAgo
-        return NSPredicate(
-            format: "date >= %@ AND enabled == %@ AND isUploadedToNS == %@",
-            date as NSDate,
-            true as NSNumber,
-            false as NSNumber
-        )
-    }
 }
 }
 
 
 extension OverrideStored {
 extension OverrideStored {

+ 87 - 3
Model/Helper/PumpEvent+helper.swift

@@ -102,7 +102,7 @@ struct BolusDTO: Codable {
     var isExternal: Bool
     var isExternal: Bool
     var isSMB: Bool
     var isSMB: Bool
     var duration: Int
     var duration: Int
-    var _type: String = "Bolus"
+    var _type: String = EventType.bolus.rawValue
 }
 }
 
 
 struct TempBasalDTO: Codable {
 struct TempBasalDTO: Codable {
@@ -110,14 +110,14 @@ struct TempBasalDTO: Codable {
     var timestamp: String
     var timestamp: String
     var temp: String
     var temp: String
     var rate: Double
     var rate: Double
-    var _type: String = "TempBasal"
+    var _type: String = EventType.tempBasal.rawValue
 }
 }
 
 
 struct TempBasalDurationDTO: Codable {
 struct TempBasalDurationDTO: Codable {
     var id: String
     var id: String
     var timestamp: String
     var timestamp: String
     var duration: Int
     var duration: Int
-    var _type: String = "TempBasalDuration"
+    var _type: String = EventType.tempBasalDuration.rawValue
 
 
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
         case id
         case id
@@ -127,11 +127,39 @@ struct TempBasalDurationDTO: Codable {
     }
     }
 }
 }
 
 
+struct SuspendDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.pumpSuspend.rawValue
+}
+
+struct ResumeDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.pumpResume.rawValue
+}
+
+struct RewindDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.rewind.rawValue
+}
+
+struct PrimeDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.prime.rawValue
+}
+
 // Mask distinct DTO subtypes with a common enum that conforms to Encodable
 // Mask distinct DTO subtypes with a common enum that conforms to Encodable
 enum PumpEventDTO: Encodable {
 enum PumpEventDTO: Encodable {
     case bolus(BolusDTO)
     case bolus(BolusDTO)
     case tempBasal(TempBasalDTO)
     case tempBasal(TempBasalDTO)
     case tempBasalDuration(TempBasalDurationDTO)
     case tempBasalDuration(TempBasalDurationDTO)
+    case suspend(SuspendDTO)
+    case resume(ResumeDTO)
+    case rewind(RewindDTO)
+    case prime(PrimeDTO)
 
 
     func encode(to encoder: Encoder) throws {
     func encode(to encoder: Encoder) throws {
         switch self {
         switch self {
@@ -141,6 +169,14 @@ enum PumpEventDTO: Encodable {
             try tempBasal.encode(to: encoder)
             try tempBasal.encode(to: encoder)
         case let .tempBasalDuration(tempBasalDuration):
         case let .tempBasalDuration(tempBasalDuration):
             try tempBasalDuration.encode(to: encoder)
             try tempBasalDuration.encode(to: encoder)
+        case let .suspend(suspend):
+            try suspend.encode(to: encoder)
+        case let .resume(resume):
+            try resume.encode(to: encoder)
+        case let .rewind(rewind):
+            try rewind.encode(to: encoder)
+        case let .prime(prime):
+            try prime.encode(to: encoder)
         }
         }
     }
     }
 }
 }
@@ -195,4 +231,52 @@ extension PumpEventStored {
         )
         )
         return .tempBasalDuration(tempBasalDurationDTO)
         return .tempBasalDuration(tempBasalDurationDTO)
     }
     }
+
+    func toPumpSuspendDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpSuspend.rawValue else {
+            return nil
+        }
+
+        let suspendDTO = SuspendDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .suspend(suspendDTO)
+    }
+
+    func toPumpResumeDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpResume.rawValue else {
+            return nil
+        }
+
+        let resumeDTO = ResumeDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .resume(resumeDTO)
+    }
+
+    func toRewindDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.rewind.rawValue else {
+            return nil
+        }
+
+        let rewindDTO = RewindDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .rewind(rewindDTO)
+    }
+
+    func toPrimeDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.prime.rawValue else {
+            return nil
+        }
+
+        let primeDTO = PrimeDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .prime(primeDTO)
+    }
 }
 }

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit 1fa2874419225c8c7af0d9afbd9faf823cda34e5
+Subproject commit 6f65cbae4c8089a892911e273204edfc4cc81e9d

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 48a35efa52f42e0b72fe2e984f60d4482a11a75f
+Subproject commit 39915b05fe46b5d73eca52e156dd7efd11193ee8

+ 11 - 1
Trio Watch App Extension/Helper/Helper+ButtonStyles.swift

@@ -5,6 +5,8 @@ struct WatchOSButtonStyle: ButtonStyle {
     var foregroundColor: Color = .white
     var foregroundColor: Color = .white
     var fontSize: Font = .title2
     var fontSize: Font = .title2
 
 
+    @Environment(\.isEnabled) private var isEnabled: Bool
+
     private var fontWeight: Font.Weight {
     private var fontWeight: Font.Weight {
         switch deviceType {
         switch deviceType {
         case .watch40mm:
         case .watch40mm:
@@ -44,11 +46,19 @@ struct WatchOSButtonStyle: ButtonStyle {
     }
     }
 
 
     func makeBody(configuration: Configuration) -> some View {
     func makeBody(configuration: Configuration) -> some View {
+        var buttonBackground: Color {
+            if isEnabled {
+                return Color.tabBar.opacity(configuration.isPressed ? 0.8 : 1.0)
+            } else {
+                return Color.tabBar.opacity(0.4)
+            }
+        }
+
         configuration.label
         configuration.label
             .font(fontSize)
             .font(fontSize)
             .fontWeight(fontWeight)
             .fontWeight(fontWeight)
             .padding(buttonPadding)
             .padding(buttonPadding)
-            .background(Color.tabBar.opacity(configuration.isPressed ? 0.8 : 1.0))
+            .background(buttonBackground)
             .clipShape(Circle())
             .clipShape(Circle())
             .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
             .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
     }
     }

+ 20 - 0
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -16,6 +16,25 @@ struct BolusProgressOverlay: View {
         endPoint: .trailing
         endPoint: .trailing
     )
     )
 
 
+    private var isWatchStateDated: Bool {
+        // If `lastWatchStateUpdate` is nil, treat as "dated"
+        guard let lastUpdateTimestamp = state.lastWatchStateUpdate else {
+            return true
+        }
+        let now = Date().timeIntervalSince1970
+        let secondsSinceUpdate = now - lastUpdateTimestamp
+        // Return true if last update older than 5 min, so 1 loop cycle
+        return secondsSinceUpdate > 5 * 60
+    }
+
+    private var isSessionUnreachable: Bool {
+        guard let session = state.session else {
+            return true // No session at all => unreachable
+        }
+        // Return true if not .activated OR not reachable
+        return session.activationState != .activated
+    }
+
     var body: some View {
     var body: some View {
         VStack(spacing: 10) {
         VStack(spacing: 10) {
             VStack {
             VStack {
@@ -45,6 +64,7 @@ struct BolusProgressOverlay: View {
                 }
                 }
                 .buttonStyle(.bordered)
                 .buttonStyle(.bordered)
                 .padding()
                 .padding()
+                .disabled(isWatchStateDated || isSessionUnreachable)
             }
             }
             .padding()
             .padding()
             .background(Color.black.opacity(0.9))
             .background(Color.black.opacity(0.9))

+ 5 - 0
Trio Watch App Extension/Views/GlucoseChartView.swift

@@ -6,6 +6,8 @@ import SwiftUI
 
 
 struct GlucoseChartView: View {
 struct GlucoseChartView: View {
     let glucoseValues: [(date: Date, glucose: Double, color: Color)]
     let glucoseValues: [(date: Date, glucose: Double, color: Color)]
+    let minYAxisValue: Decimal
+    let maxYAxisValue: Decimal
     @State private var timeWindow: TimeWindow = .threeHours
     @State private var timeWindow: TimeWindow = .threeHours
 
 
     enum TimeWindow: Int {
     enum TimeWindow: Int {
@@ -69,6 +71,9 @@ struct GlucoseChartView: View {
                         }
                         }
                     }
                     }
                 }
                 }
+                .chartYScale(
+                    domain: minYAxisValue ... maxYAxisValue
+                )
                 .chartPlotStyle { plotContent in
                 .chartPlotStyle { plotContent in
                     plotContent
                     plotContent
                         .background(
                         .background(

+ 13 - 4
Trio Watch App Extension/Views/TrioMainWatchView.swift

@@ -88,8 +88,12 @@ struct TrioMainWatchView: View {
                 }.tag(0)
                 }.tag(0)
 
 
                 // Page 2: Glucose chart
                 // Page 2: Glucose chart
-                GlucoseChartView(glucoseValues: state.glucoseValues)
-                    .tag(1)
+                GlucoseChartView(
+                    glucoseValues: state.glucoseValues,
+                    minYAxisValue: state.minYAxisValue,
+                    maxYAxisValue: state.maxYAxisValue
+                )
+                .tag(1)
             }
             }
             .onAppear {
             .onAppear {
                 // Hard reset variables when main view appears
                 // Hard reset variables when main view appears
@@ -135,7 +139,9 @@ struct TrioMainWatchView: View {
                     } label: {
                     } label: {
                         Image(systemName: "clock.arrow.2.circlepath")
                         Image(systemName: "clock.arrow.2.circlepath")
                             .foregroundStyle(Color.primary, isOverrideActive ? Color.primary : Color.purple)
                             .foregroundStyle(Color.primary, isOverrideActive ? Color.primary : Color.purple)
-                    }.tint(isOverrideActive ? Color.purple : nil)
+                    }
+                    .tint(isOverrideActive ? Color.purple : nil)
+                    .disabled(isWatchStateDated || isSessionUnreachable)
 
 
                     Button {
                     Button {
                         showingTreatmentMenuSheet = true
                         showingTreatmentMenuSheet = true
@@ -145,13 +151,16 @@ struct TrioMainWatchView: View {
                     }
                     }
                     .controlSize(.large)
                     .controlSize(.large)
                     .buttonStyle(WatchOSButtonStyle(deviceType: state.deviceType))
                     .buttonStyle(WatchOSButtonStyle(deviceType: state.deviceType))
+                    .disabled(isWatchStateDated || isSessionUnreachable)
 
 
                     Button {
                     Button {
                         showingTempTargetSheet = true
                         showingTempTargetSheet = true
                     } label: {
                     } label: {
                         Image(systemName: "target")
                         Image(systemName: "target")
                             .foregroundStyle(isTempTargetActive ? Color.primary : Color.loopGreen.opacity(0.75))
                             .foregroundStyle(isTempTargetActive ? Color.primary : Color.loopGreen.opacity(0.75))
-                    }.tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
+                    }
+                    .tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
+                    .disabled(isWatchStateDated || isSessionUnreachable)
                 }
                 }
             }
             }
             .fullScreenCover(isPresented: $showingTreatmentMenuSheet) {
             .fullScreenCover(isPresented: $showingTreatmentMenuSheet) {

+ 14 - 0
Trio Watch App Extension/WatchState.swift

@@ -20,6 +20,8 @@ import WatchConnectivity
     var trend: String? = ""
     var trend: String? = ""
     var delta: String? = "--"
     var delta: String? = "--"
     var glucoseValues: [(date: Date, glucose: Double, color: Color)] = []
     var glucoseValues: [(date: Date, glucose: Double, color: Color)] = []
+    var minYAxisValue: Decimal = 39
+    var maxYAxisValue: Decimal = 200
     var cob: String? = "--"
     var cob: String? = "--"
     var iob: String? = "--"
     var iob: String? = "--"
     var lastLoopTime: String? = "--"
     var lastLoopTime: String? = "--"
@@ -518,6 +520,18 @@ import WatchConnectivity
             .sorted { $0.date < $1.date }
             .sorted { $0.date < $1.date }
         }
         }
 
 
+        if let minYAxisValue = message[WatchMessageKeys.minYAxisValue] {
+            if let decimalValue = (minYAxisValue as? NSNumber)?.decimalValue {
+                self.minYAxisValue = decimalValue
+            }
+        }
+
+        if let maxYAxisValue = message[WatchMessageKeys.maxYAxisValue] {
+            if let decimalValue = (maxYAxisValue as? NSNumber)?.decimalValue {
+                self.maxYAxisValue = decimalValue
+            }
+        }
+
         if let overrideData = message[WatchMessageKeys.overridePresets] as? [[String: Any]] {
         if let overrideData = message[WatchMessageKeys.overridePresets] as? [[String: Any]] {
             overridePresets = overrideData.compactMap { data in
             overridePresets = overrideData.compactMap { data in
                 guard let name = data["name"] as? String,
                 guard let name = data["name"] as? String,

+ 27 - 15
Trio.xcodeproj/project.pbxproj

@@ -204,7 +204,7 @@
 		38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */; };
 		38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */; };
 		38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3F92737E42000574A46 /* BaseStateModel.swift */; };
 		38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3F92737E42000574A46 /* BaseStateModel.swift */; };
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
-		38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMProvider.swift */; };
+		38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		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 */; };
@@ -212,6 +212,7 @@
 		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 */; };
+		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.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 */; };
@@ -286,7 +287,7 @@
 		B7C465E9472624D8A2BE2A6A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		B7C465E9472624D8A2BE2A6A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
-		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
+		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
@@ -367,6 +368,7 @@
 		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
 		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
 		CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */; };
 		CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
+		CE3EEF9A2D463717001944DD /* CustomCGMOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
 		CE51DD1C2A01970900F163F7 /* ConnectIQ 2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */; };
 		CE51DD1C2A01970900F163F7 /* ConnectIQ 2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */; };
@@ -414,6 +416,7 @@
 		CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */; };
 		CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */; };
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
+		CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
@@ -565,7 +568,7 @@
 		E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3712CEEC038009A472C /* ContactImageRootView.swift */; };
 		E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3712CEEC038009A472C /* ContactImageRootView.swift */; };
 		E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3742CEEC038009A472C /* ContactImageProvider.swift */; };
 		E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3742CEEC038009A472C /* ContactImageProvider.swift */; };
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
-		F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; };
+		F5CA3DB1F9DC8B05792BBFAA /* CGMSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */; };
 		F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; };
 		F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; };
 		F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825D28DB441200054060 /* HeartBeatManager.swift */; };
 		F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825D28DB441200054060 /* HeartBeatManager.swift */; };
 		F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825F28DB441800054060 /* BluetoothTransmitter.swift */; };
 		F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825F28DB441800054060 /* BluetoothTransmitter.swift */; };
@@ -928,7 +931,7 @@
 		38FE826C25CC8461001FF17A /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = "<group>"; };
 		38FE826C25CC8461001FF17A /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = "<group>"; };
 		38FEF3F92737E42000574A46 /* BaseStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStateModel.swift; sourceTree = "<group>"; };
 		38FEF3F92737E42000574A46 /* BaseStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
-		38FEF3FD2738083E00574A46 /* CGMProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMProvider.swift; sourceTree = "<group>"; };
+		38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMSettingsProvider.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		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>"; };
@@ -940,6 +943,7 @@
 		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>"; };
+		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.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>"; };
@@ -978,7 +982,7 @@
 		5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadView.swift; sourceTree = "<group>"; };
 		5A2325512BFCBF55003518CA /* NightscoutUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadView.swift; sourceTree = "<group>"; };
 		5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutFetchView.swift; sourceTree = "<group>"; };
 		5A2325532BFCBF65003518CA /* NightscoutFetchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutFetchView.swift; sourceTree = "<group>"; };
 		5A2325572BFCC168003518CA /* NightscoutConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutConnectView.swift; sourceTree = "<group>"; };
 		5A2325572BFCC168003518CA /* NightscoutConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutConnectView.swift; sourceTree = "<group>"; };
-		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
+		5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
 		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CarbRatioEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorStateModel.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CarbRatioEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1012,7 +1016,7 @@
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
 		B5822B15939E719628E9FF7C /* SnoozeRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeRootView.swift; sourceTree = "<group>"; };
 		B5822B15939E719628E9FF7C /* SnoozeRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeRootView.swift; sourceTree = "<group>"; };
-		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
+		B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsDataFlow.swift; sourceTree = "<group>"; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
@@ -1097,6 +1101,7 @@
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D17297C9EE800DF218F /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE398D17297C9EE800DF218F /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE398D1A297D69A900DF218F /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE398D1A297D69A900DF218F /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCGMOptionsView.swift; sourceTree = "<group>"; };
 		CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBLEPumpManagerExtensions.swift; sourceTree = "<group>"; };
 		CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBLEPumpManagerExtensions.swift; sourceTree = "<group>"; };
 		CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniPodManagerExtensions.swift; sourceTree = "<group>"; };
 		CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniPodManagerExtensions.swift; sourceTree = "<group>"; };
 		CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "ConnectIQ 2.xcframework"; path = "Dependencies/ConnectIQ 2.xcframework"; sourceTree = "<group>"; };
 		CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "ConnectIQ 2.xcframework"; path = "Dependencies/ConnectIQ 2.xcframework"; sourceTree = "<group>"; };
@@ -1144,6 +1149,7 @@
 		CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsTests.swift; sourceTree = "<group>"; };
 		CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsTests.swift; sourceTree = "<group>"; };
+		CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMOptions.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsStateModel.swift; sourceTree = "<group>"; };
 		DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1402,6 +1408,7 @@
 		0D76BBC81CEDC1A0050F45EF /* View */ = {
 		0D76BBC81CEDC1A0050F45EF /* View */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */,
 				38569352270B5E350002C50D /* CGMRootView.swift */,
 				38569352270B5E350002C50D /* CGMRootView.swift */,
 				CE7950232997D81700FA576E /* CGMSettingsView.swift */,
 				CE7950232997D81700FA576E /* CGMSettingsView.swift */,
 				CE7950252998056D00FA576E /* CGMSetupView.swift */,
 				CE7950252998056D00FA576E /* CGMSetupView.swift */,
@@ -1619,7 +1626,7 @@
 				DD09D4792C5986BA003FEA5D /* CalendarEventSettings */,
 				DD09D4792C5986BA003FEA5D /* CalendarEventSettings */,
 				CEE9A64D2BBB411C00EB5194 /* Calibrations */,
 				CEE9A64D2BBB411C00EB5194 /* Calibrations */,
 				E42231DBF0DBE2B4B92D1B15 /* CarbRatioEditor */,
 				E42231DBF0DBE2B4B92D1B15 /* CarbRatioEditor */,
-				F75CB57ED6971B46F8756083 /* CGM */,
+				F75CB57ED6971B46F8756083 /* CGMSettings */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E592A3762CEEC038009A472C /* ContactImage */,
 				E592A3762CEEC038009A472C /* ContactImage */,
 				9E56E3626FAD933385101B76 /* DataTable */,
 				9E56E3626FAD933385101B76 /* DataTable */,
@@ -2119,6 +2126,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
@@ -2348,6 +2356,7 @@
 		5825D1622BD405AE00F36E9B /* Helper */ = {
 		5825D1622BD405AE00F36E9B /* Helper */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */,
 				581516A82BCEEDF800BF67D7 /* NSPredicates.swift */,
 				581516A82BCEEDF800BF67D7 /* NSPredicates.swift */,
 				583684052BD178DB00070A60 /* GlucoseStored+helper.swift */,
 				583684052BD178DB00070A60 /* GlucoseStored+helper.swift */,
 				58F107732BD1A4D000B1A680 /* Determination+helper.swift */,
 				58F107732BD1A4D000B1A680 /* Determination+helper.swift */,
@@ -3142,15 +3151,15 @@
 			path = GlucoseNotificationSettings;
 			path = GlucoseNotificationSettings;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
-		F75CB57ED6971B46F8756083 /* CGM */ = {
+		F75CB57ED6971B46F8756083 /* CGMSettings */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */,
-				38FEF3FD2738083E00574A46 /* CGMProvider.swift */,
-				5C018D1680307A31C9ED7120 /* CGMStateModel.swift */,
+				B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */,
+				38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */,
+				5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */,
 				0D76BBC81CEDC1A0050F45EF /* View */,
 				0D76BBC81CEDC1A0050F45EF /* View */,
 			);
 			);
-			path = CGM;
+			path = CGMSettings;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
 		F90692A8274B7A980037068D /* HealthKit */ = {
 		F90692A8274B7A980037068D /* HealthKit */ = {
@@ -3780,6 +3789,7 @@
 				DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */,
 				DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */,
 				DD2CC85C2D25DA1000445446 /* GlucoseTargetsView.swift in Sources */,
 				DD2CC85C2D25DA1000445446 /* GlucoseTargetsView.swift in Sources */,
 				190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */,
 				190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */,
+				CE3EEF9A2D463717001944DD /* CustomCGMOptionsView.swift in Sources */,
 				5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */,
 				5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,
@@ -3825,6 +3835,7 @@
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
 				19D466AA29AA3099004D5F33 /* MealSettingsRootView.swift in Sources */,
+				CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */,
 				DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
@@ -3836,7 +3847,7 @@
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
-				38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */,
+				38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */,
 				CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */,
 				DD1745242C55526000211FAC /* SMBSettingsStateModel.swift in Sources */,
 				DD1745242C55526000211FAC /* SMBSettingsStateModel.swift in Sources */,
@@ -3954,6 +3965,7 @@
 				DD1745222C55524800211FAC /* SMBSettingsProvider.swift in Sources */,
 				DD1745222C55524800211FAC /* SMBSettingsProvider.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */,
 				583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */,
+				49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */,
 				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,
 				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
@@ -3979,9 +3991,9 @@
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
-				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
+				F5CA3DB1F9DC8B05792BBFAA /* CGMSettingsDataFlow.swift in Sources */,
 				BDF34F952C10D27300D51995 /* DeterminationData.swift in Sources */,
 				BDF34F952C10D27300D51995 /* DeterminationData.swift in Sources */,
-				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
+				BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */,
 				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,

+ 9 - 3
Trio/Sources/APS/APSManager.swift

@@ -22,7 +22,7 @@ protocol APSManager {
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func determineBasal() async -> Bool
     func determineBasal() async -> Bool
     func determineBasalSync() async
     func determineBasalSync() async
-    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination?
+    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
@@ -413,10 +413,16 @@ final class BaseAPSManager: APSManager, Injectable {
         _ = await determineBasal()
         _ = await determineBasal()
     }
     }
 
 
-    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination? {
+    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
         do {
         do {
             let temp = await fetchCurrentTempBasal(date: Date.now)
             let temp = await fetchCurrentTempBasal(date: Date.now)
-            return try await openAPS.determineBasal(currentTemp: temp, clock: Date(), carbs: carbs, iob: iob, simulation: true)
+            return try await openAPS.determineBasal(
+                currentTemp: temp,
+                clock: Date(),
+                simulatedCarbsAmount: simulatedCarbsAmount,
+                simulatedBolusAmount: simulatedBolusAmount,
+                simulation: true
+            )
         } catch {
         } catch {
             debugPrint(
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"

+ 5 - 5
Trio/Sources/APS/CGM/CGMType.swift

@@ -14,7 +14,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
         case .none:
         case .none:
             return "None"
             return "None"
         case .nightscout:
         case .nightscout:
-            return "Nightscout"
+            return "Nightscout as CGM"
         case .xdrip:
         case .xdrip:
             return "xDrip4iOS"
             return "xDrip4iOS"
         case .simulator:
         case .simulator:
@@ -22,7 +22,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
         case .enlite:
         case .enlite:
             return "Medtronic Enlite"
             return "Medtronic Enlite"
         case .plugin:
         case .plugin:
-            return "plugin CGM"
+            return "Plugin CGM"
         }
         }
     }
     }
 
 
@@ -52,16 +52,16 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     var subtitle: String {
     var subtitle: String {
         switch self {
         switch self {
         case .none:
         case .none:
-            return NSLocalizedString("None", comment: "No CGM choiced")
+            return NSLocalizedString("None", comment: "No CGM selected")
         case .nightscout:
         case .nightscout:
-            return NSLocalizedString("Online or internal server", comment: "Online or internal server")
+            return NSLocalizedString("Uses your Nightscout as CGM", comment: "Online or internal server")
         case .xdrip:
         case .xdrip:
             return NSLocalizedString(
             return NSLocalizedString(
                 "Using shared app group with external CGM app xDrip4iOS",
                 "Using shared app group with external CGM app xDrip4iOS",
                 comment: "Shared app group xDrip4iOS"
                 comment: "Shared app group xDrip4iOS"
             )
             )
         case .simulator:
         case .simulator:
-            return NSLocalizedString("Simple simulator", comment: "Simple simulator")
+            return NSLocalizedString("Glucose Simulator for Demo Only", comment: "Simple simulator")
         case .enlite:
         case .enlite:
             return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
             return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
         case .plugin:
         case .plugin:

+ 1 - 2
Trio/Sources/APS/CGM/PluginSource.swift

@@ -107,8 +107,7 @@ extension PluginSource: CGMManagerDelegate {
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         dispatchPrecondition(condition: .onQueue(processQueue))
         debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
         debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        // TODO:
-        glucoseManager?.cgmGlucoseSourceType = .none
+        glucoseManager?.deleteGlucoseSource()
     }
     }
 
 
     func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {
     func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {

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

@@ -415,10 +415,6 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             settingsManager.updateInsulinCurve(status.insulinType)
             settingsManager.updateInsulinCurve(status.insulinType)
         }
         }
 
 
-        broadcaster.notify(PumpTimeZoneObserver.self, on: processQueue) {
-            $0.pumpTimeZoneDidChange(status.timeZone)
-        }
-
         if let omnipod = pumpManager as? OmnipodPumpManager {
         if let omnipod = pumpManager as? OmnipodPumpManager {
             let reservoirVal = omnipod.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
             let reservoirVal = omnipod.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
             // TODO: find the value Pod.maximumReservoirReading
             // TODO: find the value Pod.maximumReservoirReading
@@ -700,10 +696,6 @@ protocol PumpBatteryObserver {
     func pumpBatteryDidChange(_ battery: Battery)
     func pumpBatteryDidChange(_ battery: Battery)
 }
 }
 
 
-protocol PumpTimeZoneObserver {
-    func pumpTimeZoneDidChange(_ timezone: TimeZone)
-}
-
 protocol PumpDeactivatedObserver {
 protocol PumpDeactivatedObserver {
     func pumpDeactivatedDidChange()
     func pumpDeactivatedDidChange()
 }
 }

+ 1 - 0
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -129,6 +129,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager
         if let manager = newManager
         {
         {
             cgmManager = manager
             cgmManager = manager
+            glucoseSource = nil
             removeCalibrations()
             removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             cgmManager = cgmManagerFromRawValue(rawCGMManager)

+ 25 - 10
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -189,7 +189,10 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    private func parsePumpHistory(_ pumpHistoryObjectIDs: [NSManagedObjectID], iob: Decimal? = nil) async -> String {
+    private func parsePumpHistory(
+        _ pumpHistoryObjectIDs: [NSManagedObjectID],
+        simulatedBolusAmount: Decimal? = nil
+    ) async -> 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 "{}" }
 
 
@@ -199,9 +202,9 @@ final class OpenAPS {
             var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
             var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
 
 
             // Optionally add the IOB as a DTO
             // Optionally add the IOB as a DTO
-            if let iob = iob {
-                let iobDTO = self.createIOBDTO(iob: iob)
-                dtos.insert(iobDTO, at: 0)
+            if let simulatedBolusAmount = simulatedBolusAmount {
+                let simulatedBolusDTO = self.createSimulatedBolusDTO(simulatedBolusAmount: simulatedBolusAmount)
+                dtos.insert(simulatedBolusDTO, at: 0)
             }
             }
 
 
             // Convert the DTOs to JSON
             // Convert the DTOs to JSON
@@ -226,12 +229,24 @@ final class OpenAPS {
             if let tempBasalDTO = event.toTempBasalDTOEnum() {
             if let tempBasalDTO = event.toTempBasalDTOEnum() {
                 eventDTOs.append(tempBasalDTO)
                 eventDTOs.append(tempBasalDTO)
             }
             }
+            if let pumpSuspendDTO = event.toPumpSuspendDTO() {
+                eventDTOs.append(pumpSuspendDTO)
+            }
+            if let pumpResumeDTO = event.toPumpResumeDTO() {
+                eventDTOs.append(pumpResumeDTO)
+            }
+            if let rewindDTO = event.toRewindDTO() {
+                eventDTOs.append(rewindDTO)
+            }
+            if let primeDTO = event.toPrimeDTO() {
+                eventDTOs.append(primeDTO)
+            }
             return eventDTOs
             return eventDTOs
         }
         }
         return dtos
         return dtos
     }
     }
 
 
-    private func createIOBDTO(iob: Decimal) -> PumpEventDTO {
+    private func createSimulatedBolusDTO(simulatedBolusAmount: Decimal) -> PumpEventDTO {
         let oneSecondAgo = Calendar.current
         let oneSecondAgo = Calendar.current
             .date(
             .date(
                 byAdding: .second,
                 byAdding: .second,
@@ -243,7 +258,7 @@ final class OpenAPS {
         let bolusDTO = BolusDTO(
         let bolusDTO = BolusDTO(
             id: UUID().uuidString,
             id: UUID().uuidString,
             timestamp: dateFormatted,
             timestamp: dateFormatted,
-            amount: Double(iob),
+            amount: Double(simulatedBolusAmount),
             isExternal: false,
             isExternal: false,
             isSMB: true,
             isSMB: true,
             duration: 0,
             duration: 0,
@@ -255,8 +270,8 @@ final class OpenAPS {
     func determineBasal(
     func determineBasal(
         currentTemp: TempBasal,
         currentTemp: TempBasal,
         clock: Date = Date(),
         clock: Date = Date(),
-        carbs: Decimal? = nil,
-        iob: Decimal? = nil,
+        simulatedCarbsAmount: Decimal? = nil,
+        simulatedBolusAmount: Decimal? = nil,
         simulation: Bool = false
         simulation: Bool = false
     ) async throws -> Determination? {
     ) async throws -> Determination? {
         debug(.openAPS, "Start determineBasal")
         debug(.openAPS, "Start determineBasal")
@@ -266,7 +281,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: carbs ?? 0)
+        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0)
         async let glucose = fetchAndProcessGlucose()
         async let glucose = fetchAndProcessGlucose()
         async let oref2 = oref2()
         async let oref2 = oref2()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
@@ -287,7 +302,7 @@ final class OpenAPS {
             reservoir,
             reservoir,
             preferences
             preferences
         ) = await (
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs, iob: iob),
+            parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
             carbs,
             carbs,
             glucose,
             glucose,
             oref2,
             oref2,

+ 1 - 1
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -210,7 +210,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
-            predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout,
+            predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             key: "date",
             ascending: false
             ascending: false
         )
         )

+ 4 - 2
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -94,7 +94,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
 
 
                         let newPumpEvent = PumpEventStored(context: self.context)
                         let newPumpEvent = PumpEventStored(context: self.context)
                         newPumpEvent.id = UUID().uuidString
                         newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
+                        // restrict entry to now or past
+                        newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToHealth = false
                         newPumpEvent.isUploadedToHealth = false
@@ -232,7 +233,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             // create pump event
             // create pump event
             let newPumpEvent = PumpEventStored(context: self.context)
             let newPumpEvent = PumpEventStored(context: self.context)
             newPumpEvent.id = UUID().uuidString
             newPumpEvent.id = UUID().uuidString
-            newPumpEvent.timestamp = timestamp
+            // restrict entry to now or past
+            newPumpEvent.timestamp = timestamp > Date() ? Date() : timestamp
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.isUploadedToNS = false
             newPumpEvent.isUploadedToNS = false
             newPumpEvent.isUploadedToHealth = false
             newPumpEvent.isUploadedToHealth = false

+ 3 - 3
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -14,7 +14,7 @@ protocol TempTargetsStorage {
     func fetchScheduledTempTargets() async -> [NSManagedObjectID]
     func fetchScheduledTempTargets() async -> [NSManagedObjectID]
     func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
     func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
-    func deleteOverridePreset(_ objectID: NSManagedObjectID) async
+    func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
     func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
     func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [TempTarget]
     func recent() -> [TempTarget]
@@ -216,7 +216,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return newTempTarget.objectID
         return newTempTarget.objectID
     }
     }
 
 
-    @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
+    @MainActor func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async {
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
     }
     }
 
 
@@ -246,7 +246,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
-            predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout, // TODO: create adjustment predicate (OR+TT)
+            predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             key: "date",
             ascending: false
             ascending: false
         )
         )

+ 24 - 9
Trio/Sources/Application/TrioApp.swift

@@ -13,7 +13,7 @@ import Swinject
     // Read the color scheme preference from UserDefaults; defaults to system default setting
     // Read the color scheme preference from UserDefaults; defaults to system default setting
     @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
     @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
 
 
-    let coreDataStack = CoreDataStack.shared
+    let coreDataStack: CoreDataStack
 
 
     @State private var appState = AppState()
     @State private var appState = AppState()
 
 
@@ -67,15 +67,30 @@ import Swinject
             .default,
             .default,
             "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))]"
             "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))]"
         )
         )
+        
+        // Setup up the Core Data Stack
+        coreDataStack = CoreDataStack.shared
 
 
-        // Load services
-        loadServices()
-
-        // Fix bug in iOS 18 related to the translucent tab bar
-        configureTabBarAppearance()
-
-        // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
-        cleanupOldData()
+        do {
+            // Explicitly initialize Core Data Stacak
+            try coreDataStack.initializeStack()
+            
+            // Load services
+            loadServices()
+            
+            // Fix bug in iOS 18 related to the translucent tab bar
+            configureTabBarAppearance()
+            
+            // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
+            cleanupOldData()
+        } catch {
+            debug(
+                .coreData,
+                "Failed to initialize Core Data Stack: \(error.localizedDescription)"
+            )
+            // Handle initialization failure
+            fatalError("Core Data Stack initialization failed: \(error.localizedDescription)")
+        }
     }
     }
 
 
     var body: some Scene {
     var body: some Scene {

+ 15 - 0
Trio/Sources/Helpers/CGMOptions.swift

@@ -0,0 +1,15 @@
+let cgmOptions: [CGMOption] = [
+    CGMOption(name: "Dexcom G5", predicate: { $0.type == .plugin && $0.displayName.contains("G5") }),
+    CGMOption(name: "Dexcom G6 / ONE", predicate: { $0.type == .plugin && $0.displayName.contains("G6") }),
+    CGMOption(name: "Dexcom G7 / ONE+", predicate: { $0.type == .plugin && $0.displayName.contains("G7") }),
+    CGMOption(name: "Dexcom Share", predicate: { $0.type == .plugin && $0.displayName.contains("Dexcom Share") }),
+    CGMOption(name: "FreeStyle Libre", predicate: { $0.type == .plugin && $0.displayName == "FreeStyle Libre" }),
+    CGMOption(
+        name: "FreeStyle Libre Demo",
+        predicate: { $0.type == .plugin && $0.displayName == "FreeStyle Libre Demo" }
+    ),
+    CGMOption(name: "Glucose Simulator", predicate: { $0.type == .simulator }),
+    CGMOption(name: "Medtronic Enlite", predicate: { $0.type == .enlite }),
+    CGMOption(name: "Nightscout as CGM", predicate: { $0.type == .nightscout }),
+    CGMOption(name: "xDrip4iOS", predicate: { $0.type == .xdrip })
+]

+ 5 - 1
Trio/Sources/Logger/Logger.swift

@@ -115,6 +115,7 @@ final class Logger {
     static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
     static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
     static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
     static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
     static let watchManager = Logger(category: .watchManager, reporter: baseReporter)
     static let watchManager = Logger(category: .watchManager, reporter: baseReporter)
+    static let coreData = Logger(category: .coreData, reporter: baseReporter)
 
 
     enum Category: String {
     enum Category: String {
         case `default`
         case `default`
@@ -127,6 +128,7 @@ final class Logger {
         case remoteControl
         case remoteControl
         case bolusState
         case bolusState
         case watchManager
         case watchManager
+        case coreData
 
 
         var name: String {
         var name: String {
             rawValue.capitalizingFirstLetter()
             rawValue.capitalizingFirstLetter()
@@ -144,6 +146,7 @@ final class Logger {
             case .remoteControl: return .remoteControl
             case .remoteControl: return .remoteControl
             case .bolusState: return .bolusState
             case .bolusState: return .bolusState
             case .watchManager: return .watchManager
             case .watchManager: return .watchManager
+            case .coreData: return .coreData
             }
             }
         }
         }
 
 
@@ -159,7 +162,8 @@ final class Logger {
                  .openAPS,
                  .openAPS,
                  .remoteControl,
                  .remoteControl,
                  .service,
                  .service,
-                 .watchManager:
+                 .watchManager,
+                 .coreData:
                 return OSLog(subsystem: subsystem, category: name)
                 return OSLog(subsystem: subsystem, category: name)
             }
             }
         }
         }

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

@@ -37,6 +37,8 @@ enum WatchMessageKeys {
     static let cob = "cob"
     static let cob = "cob"
     static let lastLoopTime = "lastLoopTime"
     static let lastLoopTime = "lastLoopTime"
     static let glucoseValues = "glucoseValues"
     static let glucoseValues = "glucoseValues"
+    static let minYAxisValue = "minYAxisValue"
+    static let maxYAxisValue = "maxYAxisValue"
     static let overridePresets = "overridePresets"
     static let overridePresets = "overridePresets"
     static let tempTargetPresets = "tempTargetPresets"
     static let tempTargetPresets = "tempTargetPresets"
 
 

+ 9 - 3
Trio/Sources/Models/WatchState.swift

@@ -8,6 +8,8 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
     var trend: String?
     var trend: String?
     var delta: String?
     var delta: String?
     var glucoseValues: [WatchGlucoseObject] = []
     var glucoseValues: [WatchGlucoseObject] = []
+    var minYAxisValue: Decimal = 39.0
+    var maxYAxisValue: Decimal = 200.0
     var units: GlucoseUnits = .mgdL
     var units: GlucoseUnits = .mgdL
     var iob: String?
     var iob: String?
     var cob: String?
     var cob: String?
@@ -17,9 +19,9 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
 
 
     // Safety limits
     // Safety limits
     var maxBolus: Decimal = 10.0
     var maxBolus: Decimal = 10.0
-    var maxCarbs: Decimal = 250
-    var maxFat: Decimal = 250
-    var maxProtein: Decimal = 250
+    var maxCarbs: Decimal = 250.0
+    var maxFat: Decimal = 250.0
+    var maxProtein: Decimal = 250.0
 
 
     // Pump specific dosing increment
     // Pump specific dosing increment
     var bolusIncrement: Decimal = 0.05
     var bolusIncrement: Decimal = 0.05
@@ -34,6 +36,8 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
             zip(lhs.glucoseValues, rhs.glucoseValues).allSatisfy {
             zip(lhs.glucoseValues, rhs.glucoseValues).allSatisfy {
                 $0.0.date == $0.1.date && $0.0.glucose == $0.1.glucose && $0.0.color == $0.1.color
                 $0.0.date == $0.1.date && $0.0.glucose == $0.1.glucose && $0.0.color == $0.1.color
             } &&
             } &&
+            lhs.minYAxisValue == rhs.minYAxisValue &&
+            lhs.maxYAxisValue == rhs.maxYAxisValue &&
             lhs.units == rhs.units &&
             lhs.units == rhs.units &&
             lhs.iob == rhs.iob &&
             lhs.iob == rhs.iob &&
             lhs.cob == rhs.cob &&
             lhs.cob == rhs.cob &&
@@ -58,6 +62,8 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
             hasher.combine(value.glucose)
             hasher.combine(value.glucose)
             hasher.combine(value.color)
             hasher.combine(value.color)
         }
         }
+        hasher.combine(minYAxisValue)
+        hasher.combine(maxYAxisValue)
         hasher.combine(units)
         hasher.combine(units)
         hasher.combine(iob)
         hasher.combine(iob)
         hasher.combine(cob)
         hasher.combine(cob)

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

@@ -250,9 +250,7 @@ extension Adjustments.StateModel {
             )
             )
             tempTargetStorage.saveTempTargetsToStorage([tempTarget])
             tempTargetStorage.saveTempTargetsToStorage([tempTarget])
         } catch {
         } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact TempTarget Preset"
-            )
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact TempTarget Preset")
         }
         }
     }
     }
 
 
@@ -311,7 +309,7 @@ extension Adjustments.StateModel {
 
 
     /// Duplicates the current preset and cancels the previous one.
     /// Duplicates the current preset and cancels the previous one.
     @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
     @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
-        // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
+        // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom TempTarget
         guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
         guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
               tempTargetPresetToDuplicate.isPreset == true else { return }
               tempTargetPresetToDuplicate.isPreset == true else { return }
 
 
@@ -342,7 +340,7 @@ extension Adjustments.StateModel {
 
 
     /// Deletes a Temp Target preset.
     /// Deletes a Temp Target preset.
     func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
     func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
-        await tempTargetStorage.deleteOverridePreset(objectID)
+        await tempTargetStorage.deleteTempTargetPreset(objectID)
         setupTempTargetPresetsArray()
         setupTempTargetPresetsArray()
     }
     }
 
 

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

@@ -249,7 +249,7 @@ extension AlgorithmAdvancedSettings {
                             "Min 5m Carb Impact sets the expected glucose rise from carbs over 5 minutes when absorption isn't obvious from glucose data."
                             "Min 5m Carb Impact sets the expected glucose rise from carbs over 5 minutes when absorption isn't obvious from glucose data."
                         )
                         )
                         Text(
                         Text(
-                            "The default value of 8 mg/dL per 5 minutes corresponds to an absorption rate of 24 g of carbs per hour."
+                            "The default is an expected \(state.units == .mgdL ? "8" : 8.formattedAsMmolL) \(state.units.rawValue)/5min. This affects how fast COB is decayed in situations when carb absorption is not visible in BG deviations. The default of \(state.units == .mgdL ? "8" : 8.formattedAsMmolL) \(state.units.rawValue)/5min corresponds to a minimum carb absorption rate of 24 g/hr at a CSF of \(state.units == .mgdL ? "4" : 4.formattedAsMmolL) \(state.units.rawValue)/g."
                         )
                         )
                         Text(
                         Text(
                             "This setting helps the system estimate how much glucose your body is absorbing, even when it's not immediately visible in your glucose data, ensuring more accurate insulin dosing during carb absorption."
                             "This setting helps the system estimate how much glucose your body is absorbing, even when it's not immediately visible in your glucose data, ensuring more accurate insulin dosing during carb absorption."

+ 0 - 5
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -172,11 +172,6 @@ extension BasalProfileEditor {
             .navigationTitle("Basal Profile")
             .navigationTitle("Basal Profile")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                     Button(action: { state.add() }) {
                         HStack {
                         HStack {

+ 4 - 0
Trio/Sources/Modules/Base/BaseStateModel.swift

@@ -11,6 +11,10 @@ protocol StateModel: ObservableObject {
     func view(for screen: Screen) -> AnyView
     func view(for screen: Screen) -> AnyView
 }
 }
 
 
+protocol CGMStateModel: StateModel {
+    var cgmCurrent: CGMType { get }
+}
+
 class BaseStateModel<Provider>: StateModel, Injectable where Provider: Trio.Provider {
 class BaseStateModel<Provider>: StateModel, Injectable where Provider: Trio.Provider {
     @Injected() var router: Router!
     @Injected() var router: Router!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var settingsManager: SettingsManager!

+ 0 - 9
Trio/Sources/Modules/CGM/CGMDataFlow.swift

@@ -1,9 +0,0 @@
-enum CGM {
-    enum Config {}
-}
-
-enum cgmConfig {
-    enum Config {}
-}
-
-protocol CGMProvider: Provider {}

+ 0 - 5
Trio/Sources/Modules/CGM/CGMProvider.swift

@@ -1,5 +0,0 @@
-extension CGM {
-    final class Provider: BaseProvider, CGMProvider {
-        @Injected() var apsManager: APSManager!
-    }
-}

+ 0 - 266
Trio/Sources/Modules/CGM/View/CGMRootView.swift

@@ -1,266 +0,0 @@
-import LoopKitUI
-import SwiftUI
-import Swinject
-
-extension CGM {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        let displayClose: Bool
-        @StateObject var state = StateModel()
-        @State private var setupCGM = false
-
-        @State private var shouldDisplayHint: Bool = false
-        @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(AppState.self) var appState
-
-        var body: some View {
-            NavigationView {
-                List {
-                    Section(
-                        header: Text("CGM Integration to Trio"),
-                        content: {
-                            VStack {
-                                Picker("Type", selection: $state.cgmCurrent) {
-                                    ForEach(state.listOfCGM) { type in
-                                        VStack(alignment: .leading) {
-                                            Text(type.displayName)
-                                            Text(type.subtitle).font(.caption).foregroundColor(.secondary)
-                                        }.tag(type)
-                                    }
-                                }.padding(.top)
-
-                                HStack(alignment: .center) {
-                                    Text(
-                                        "Select your CGM. See hint for compatible devices."
-                                    )
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
-                                    .lineLimit(nil)
-                                    Spacer()
-                                    Button(
-                                        action: {
-                                            hintLabel = "Available CGM Types for Trio"
-                                            selectedVerboseHint =
-                                                AnyView(
-                                                    Text(
-                                                        "• Dexcom G5 \n• Dexcom G6 / ONE \n• Dexcom G7 / ONE+ \n• Dexcom Share \n• Freestyle Libre \n• Freestyle Libre Demo \n• Glucose Simulator \n• Medtronic Enlite \n• Nightscout \n• xDrip4iOS"
-                                                    )
-                                                )
-                                            shouldDisplayHint.toggle()
-                                        },
-                                        label: {
-                                            HStack {
-                                                Image(systemName: "questionmark.circle")
-                                            }
-                                        }
-                                    ).buttonStyle(BorderlessButtonStyle())
-                                }.padding(.top)
-                            }.padding(.bottom)
-
-                            if let link = state.cgmCurrent.type.externalLink {
-                                Button {
-                                    UIApplication.shared.open(link, options: [:], completionHandler: nil)
-                                } label: {
-                                    HStack {
-                                        Text("About this source")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
-                                    }
-                                }
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                            }
-
-                            if state.cgmCurrent.type == .plugin {
-                                Button {
-                                    setupCGM.toggle()
-                                } label: {
-                                    HStack {
-                                        Text("CGM Configuration")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
-                                    }
-                                }
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                            }
-                        }
-                    ).listRowBackground(Color.chart)
-
-                    if let appURL = state.urlOfApp() {
-                        Section {
-                            Button {
-                                UIApplication.shared.open(appURL, options: [:]) { success in
-                                    if !success {
-                                        self.router.alertMessage
-                                            .send(MessageContent(content: "Unable to open the app", type: .warning))
-                                    }
-                                }
-                            }
-
-                            label: {
-                                Label(state.displayNameOfApp() ?? "-", systemImage: "waveform.path.ecg.rectangle").font(.title3)
-                                    .padding() }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                        }
-                        .listRowBackground(Color.clear)
-                    } else if state.cgmCurrent.type == .nightscout {
-                        if let url = state.url {
-                            Section {
-                                Button {
-                                    UIApplication.shared.open(url, options: [:]) { success in
-                                        if !success {
-                                            self.router.alertMessage
-                                                .send(MessageContent(content: "No URL available", type: .warning))
-                                        }
-                                    }
-                                }
-                                label: { Label("Open URL", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding() }
-                                    .frame(maxWidth: .infinity, alignment: .center)
-                                    .buttonStyle(.bordered)
-                            }
-                            .listRowBackground(Color.clear)
-                        } else {
-                            Section {
-                                Button {
-                                    state.showModal(for: .nighscoutConfigDirect)
-                                }
-                                label: {
-                                    Label("Config Nightscout", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding()
-                                }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                            }
-                            .listRowBackground(Color.clear)
-                        }
-                    }
-
-                    if state.cgmCurrent.type == .xdrip {
-                        Section(header: Text("Heartbeat")) {
-                            VStack(alignment: .leading) {
-                                if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
-                                    Text("CGM address :").padding(.top)
-                                    Text(cgmTransmitterDeviceAddress)
-                                } else {
-                                    Text("CGM is not used as heartbeat.").padding(.top)
-                                }
-
-                                HStack(alignment: .center) {
-                                    Text(
-                                        "A heartbeat tells Trio to start a loop cycle. This is required for closed loop."
-                                    )
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
-                                    .lineLimit(nil)
-                                    Spacer()
-                                    Button(
-                                        action: {
-                                            hintLabel = "CGM Heartbeat"
-                                            selectedVerboseHint =
-                                                AnyView(
-                                                    Text(
-                                                        "The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
-                                                    )
-                                                )
-                                            shouldDisplayHint.toggle()
-                                        },
-                                        label: {
-                                            HStack {
-                                                Image(systemName: "questionmark.circle")
-                                            }
-                                        }
-                                    ).buttonStyle(BorderlessButtonStyle())
-                                }.padding(.vertical)
-                            }
-                        }.listRowBackground(Color.chart)
-                    }
-
-                    if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
-                        Section {
-                            Text("Libre Calibrations").navigationLink(to: .calibrations, from: self)
-                        }.listRowBackground(Color.chart)
-                    }
-
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.smoothGlucose,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = "Smooth Glucose Value"
-                            }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: "Smooth Glucose Value",
-                        miniHint: "Smooth CGM readings using Savitzky-Golay filtering.",
-                        verboseHint:
-                        VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text(
-                                "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
-                            )
-                            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."
-                            )
-                            Text(
-                                "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app."
-                            )
-                        }
-                    )
-                }
-                .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-                .onAppear(perform: configureView)
-                .navigationTitle("CGM")
-                .navigationBarTitleDisplayMode(.automatic)
-                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
-                .sheet(isPresented: $shouldDisplayHint) {
-                    SettingInputHintView(
-                        hintDetent: $hintDetent,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        hintLabel: hintLabel ?? "",
-                        hintText: selectedVerboseHint ?? AnyView(EmptyView()),
-                        sheetTitle: "Help"
-                    )
-                }
-                .onChange(of: setupCGM) { _, setupCGM in
-                    state.setupCGM = setupCGM
-                }
-                .onChange(of: state.setupCGM) { _, setupCGM in
-                    self.setupCGM = setupCGM
-                }
-                .screenNavigation(self)
-            }
-            .sheet(isPresented: $setupCGM) {
-                if let cgmFetchManager = state.cgmManager,
-                   let cgmManager = cgmFetchManager.cgmManager,
-                   state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
-                   state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
-                {
-                    CGMSettingsView(
-                        cgmManager: cgmManager,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        unit: state.settingsManager.settings.units,
-                        completionDelegate: state
-                    )
-                } else {
-                    CGMSetupView(
-                        CGMType: state.cgmCurrent,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        unit: state.settingsManager.settings.units,
-                        completionDelegate: state,
-                        setupDelegate: state,
-                        pluginCGMManager: self.state.pluginCGMManager
-                    )
-                }
-            }
-        }
-    }
-}

+ 5 - 0
Trio/Sources/Modules/CGMSettings/CGMSettingsDataFlow.swift

@@ -0,0 +1,5 @@
+enum CGMSettings {
+    enum Config {}
+}
+
+protocol CGMSettingsProvider: Provider {}

+ 5 - 0
Trio/Sources/Modules/CGMSettings/CGMSettingsProvider.swift

@@ -0,0 +1,5 @@
+extension CGMSettings {
+    final class Provider: BaseProvider, CGMSettingsProvider {
+        @Injected() var apsManager: APSManager!
+    }
+}

+ 69 - 65
Trio/Sources/Modules/CGM/CGMStateModel.swift

@@ -4,33 +4,60 @@ import G7SensorKit
 import LoopKitUI
 import LoopKitUI
 import SwiftUI
 import SwiftUI
 
 
-struct cgmName: Identifiable, Hashable {
+struct CGMModel: Identifiable, Hashable {
     var id: String
     var id: String
     var type: CGMType
     var type: CGMType
     var displayName: String
     var displayName: String
     var subtitle: String
     var subtitle: String
 }
 }
 
 
-let cgmDefaultName = cgmName(
+struct CGMOption {
+    let name: String
+    let predicate: (CGMModel) -> Bool
+}
+
+let cgmDefaultModel = CGMModel(
     id: CGMType.none.id,
     id: CGMType.none.id,
     type: .none,
     type: .none,
     displayName: CGMType.none.displayName,
     displayName: CGMType.none.displayName,
     subtitle: CGMType.none.subtitle
     subtitle: CGMType.none.subtitle
 )
 )
 
 
-extension CGM {
+struct OtherCGMSourceCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+class CGMSetupCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+class CGMDeletionCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+extension CGMSettings {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
-        @Injected() var cgmManager: FetchGlucoseManager!
+        // Singleton implementation
+        private static var _shared: StateModel?
+        static var shared: StateModel {
+            if _shared == nil {
+                _shared = StateModel()
+                _shared?.resolver = TrioApp().resolver
+            }
+            return _shared!
+        }
+
+        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var pluginCGMManager: PluginManager!
         @Injected() var pluginCGMManager: PluginManager!
         @Injected() private var broadcaster: Broadcaster!
         @Injected() private var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var nightscoutManager: NightscoutManager!
 
 
         @Published var units: GlucoseUnits = .mgdL
         @Published var units: GlucoseUnits = .mgdL
-        @Published var setupCGM: Bool = false
-        @Published var cgmCurrent = cgmDefaultName
+        @Published var shouldDisplayCGMSetupSheet: Bool = false
+        @Published var cgmCurrent = cgmDefaultModel
         @Published var smoothGlucose = false
         @Published var smoothGlucose = false
         @Published var cgmTransmitterDeviceAddress: String? = nil
         @Published var cgmTransmitterDeviceAddress: String? = nil
-        @Published var listOfCGM: [cgmName] = []
+        @Published var listOfCGM: [CGMModel] = []
         @Published var url: URL?
         @Published var url: URL?
 
 
         override func subscribe() {
         override func subscribe() {
@@ -39,10 +66,10 @@ extension CGM {
             // collect the list of CGM available with plugins and CGMType defined manually
             // collect the list of CGM available with plugins and CGMType defined manually
             listOfCGM = (
             listOfCGM = (
                 CGMType.allCases.filter { $0 != CGMType.plugin }.map {
                 CGMType.allCases.filter { $0 != CGMType.plugin }.map {
-                    cgmName(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+                    CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
                 } +
                 } +
                     pluginCGMManager.availableCGMManagers.map {
                     pluginCGMManager.availableCGMManagers.map {
-                        cgmName(
+                        CGMModel(
                             id: $0.identifier,
                             id: $0.identifier,
                             type: CGMType.plugin,
                             type: CGMType.plugin,
                             displayName: $0.localizedTitle,
                             displayName: $0.localizedTitle,
@@ -62,18 +89,18 @@ extension CGM {
             switch settingsManager.settings.cgm {
             switch settingsManager.settings.cgm {
             case .plugin:
             case .plugin:
                 if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
                 if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
-                    cgmCurrent = cgmName(
+                    cgmCurrent = CGMModel(
                         id: settingsManager.settings.cgmPluginIdentifier,
                         id: settingsManager.settings.cgmPluginIdentifier,
                         type: .plugin,
                         type: .plugin,
                         displayName: cgmPluginInfo.displayName,
                         displayName: cgmPluginInfo.displayName,
                         subtitle: cgmPluginInfo.subtitle
                         subtitle: cgmPluginInfo.subtitle
                     )
                     )
                 } else {
                 } else {
-                    // no more type of plugin available - restart to defaut
-                    cgmCurrent = cgmDefaultName
+                    // no more type of plugin available - fallback to default model
+                    cgmCurrent = cgmDefaultModel
                 }
                 }
             default:
             default:
-                cgmCurrent = cgmName(
+                cgmCurrent = CGMModel(
                     id: settingsManager.settings.cgm.id,
                     id: settingsManager.settings.cgm.id,
                     type: settingsManager.settings.cgm,
                     type: settingsManager.settings.cgm,
                     displayName: settingsManager.settings.cgm.displayName,
                     displayName: settingsManager.settings.cgm.displayName,
@@ -87,77 +114,54 @@ extension CGM {
                 url = URL(string: "spikeapp://")!
                 url = URL(string: "spikeapp://")!
             case "http://127.0.0.1:17580":
             case "http://127.0.0.1:17580":
                 url = URL(string: "diabox://")!
                 url = URL(string: "diabox://")!
-            //            case CGMType.libreTransmitter.appURL?.absoluteString:
-            //                showModal(for: .libreConfig)
             default: break
             default: break
             }
             }
 
 
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
 
 
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
-
-            $cgmCurrent
-                .removeDuplicates()
-                .sink { [weak self] value in
-                    guard let self = self else { return }
-                    guard self.cgmManager.cgmGlucoseSourceType != nil else {
-                        self.settingsManager.settings.cgm = .none
-                        return
-                    }
-                    if value.type != self.settingsManager.settings.cgm ||
-                        value.id != self.settingsManager.settings.cgmPluginIdentifier
-                    {
-                        self.settingsManager.settings.cgm = value.type
-                        self.settingsManager.settings.cgmPluginIdentifier = value.id
-                        self.cgmManager.updateGlucoseSource(
-                            cgmGlucoseSourceType: value.type,
-                            cgmGlucosePluginId: value.id
-                        )
-                        self.setupCGM = false
-                    }
-                }
-                .store(in: &lifetime)
         }
         }
 
 
-        func displayNameOfApp() -> String? {
-            guard cgmManager != nil else { return nil }
-            var nameOfApp = "Open Application"
-            switch cgmManager.cgmGlucoseSourceType {
+        func addCGM(cgm: CGMModel) {
+            cgmCurrent = cgm
+            switch cgmCurrent.type {
             case .plugin:
             case .plugin:
-                nameOfApp = "Open " + (cgmManager.cgmManager?.localizedTitle ?? "Application")
+                shouldDisplayCGMSetupSheet.toggle()
             default:
             default:
-                nameOfApp = "Open " + cgmManager.cgmGlucoseSourceType.displayName
+                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
             }
             }
-            return nameOfApp
         }
         }
 
 
-        func urlOfApp() -> URL? {
-            guard cgmManager != nil else { return nil }
-            switch cgmManager.cgmGlucoseSourceType {
-            case .plugin:
-                return cgmManager.cgmManager?.appURL
-            default:
-                return cgmManager.cgmGlucoseSourceType.appURL
-            }
+        func deleteCGM() {
+            shouldDisplayCGMSetupSheet = false
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
+                self.fetchGlucoseManager.deleteGlucoseSource()
+                self.completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
+            })
         }
         }
     }
     }
 }
 }
 
 
-extension CGM.StateModel: CompletionDelegate {
+extension CGMSettings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        setupCGM = false
-
         // if CGM was deleted
         // if CGM was deleted
-        if cgmManager.cgmGlucoseSourceType == nil {
-            cgmCurrent = cgmDefaultName
-            settingsManager.settings.cgm = cgmDefaultName.type
-            settingsManager.settings.cgmPluginIdentifier = cgmDefaultName.id
-            cgmManager.deleteGlucoseSource()
+        if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+            cgmCurrent = cgmDefaultModel
+            settingsManager.settings.cgm = cgmDefaultModel.type
+            settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
+            fetchGlucoseManager.deleteGlucoseSource()
+            shouldDisplayCGMSetupSheet = false
         } else {
         } else {
-            cgmManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+            settingsManager.settings.cgm = cgmCurrent.type
+            settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+            fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+            shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
+                .type == .xdrip || cgmCurrent.type == .enlite
         }
         }
 
 
-        // update if required the Glucose source
+        // update glucose source if required
         DispatchQueue.main.async {
         DispatchQueue.main.async {
             self.broadcaster.notify(GlucoseObserver.self, on: .main) {
             self.broadcaster.notify(GlucoseObserver.self, on: .main) {
                 $0.glucoseDidUpdate([])
                 $0.glucoseDidUpdate([])
@@ -166,10 +170,10 @@ extension CGM.StateModel: CompletionDelegate {
     }
     }
 }
 }
 
 
-extension CGM.StateModel: CGMManagerOnboardingDelegate {
+extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
         // update the glucose source
         // update the glucose source
-        cgmManager.updateGlucoseSource(
+        fetchGlucoseManager.updateGlucoseSource(
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucosePluginId: cgmCurrent.id,
             cgmGlucosePluginId: cgmCurrent.id,
             newManager: manager
             newManager: manager
@@ -181,7 +185,7 @@ extension CGM.StateModel: CGMManagerOnboardingDelegate {
     }
     }
 }
 }
 
 
-extension CGM.StateModel {
+extension CGMSettings.StateModel {
     func settingsDidChange(_: TrioSettings) {
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
         units = settingsManager.settings.units
     }
     }

+ 206 - 0
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -0,0 +1,206 @@
+import LoopKitUI
+import SwiftUI
+import Swinject
+
+extension CGMSettings {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        let displayClose: Bool
+        @StateObject var state = StateModel()
+
+        @State private var shouldDisplayHint: Bool = false
+        @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
+        @State var showCGMSelection: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var cgmSelectionButtons: some View {
+            ForEach(cgmOptions, id: \.name) { option in
+                if let cgm = state.listOfCGM.first(where: option.predicate) {
+                    Button(option.name) {
+                        state.addCGM(cgm: cgm)
+                    }
+                }
+            }
+        }
+
+        var body: some View {
+            NavigationView {
+                Form {
+                    Section(
+                        header: Text("CGM Integration to Trio"),
+                        content: {
+                            let cgmState = state.cgmCurrent
+                            if cgmState.type != .none {
+                                Button {
+                                    state.shouldDisplayCGMSetupSheet = true
+                                } label: {
+                                    HStack {
+                                        Image(systemName: "sensor.tag.radiowaves.forward.fill")
+                                        Text(cgmState.displayName)
+                                    }
+                                    .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
+                                    .font(.title2)
+                                }.padding()
+                            } else {
+                                VStack {
+                                    Button {
+                                        showCGMSelection.toggle()
+                                    } label: {
+                                        Text("Add CGM")
+                                            .font(.title3) }
+                                        .frame(maxWidth: .infinity, alignment: .center)
+                                        .buttonStyle(.bordered)
+
+                                    HStack(alignment: .center) {
+                                        Text(
+                                            "Pair your CGM with Trio. See hint for compatible devices."
+                                        )
+                                        .font(.footnote)
+                                        .foregroundColor(.secondary)
+                                        .lineLimit(nil)
+                                        Spacer()
+                                        Button(
+                                            action: {
+                                                shouldDisplayHint.toggle()
+                                            },
+                                            label: {
+                                                HStack {
+                                                    Image(systemName: "questionmark.circle")
+                                                }
+                                            }
+                                        ).buttonStyle(BorderlessButtonStyle())
+                                    }.padding(.top)
+                                }.padding(.vertical)
+                            }
+                        }
+                    )
+                    .listRowBackground(Color.chart)
+
+                    if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
+                        Section {
+                            NavigationLink(
+                                destination: Calibrations.RootView(resolver: resolver),
+                                label: { Text("Libre Calibrations") }
+                            )
+                        }.listRowBackground(Color.chart)
+                    }
+
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.smoothGlucose,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0.map { AnyView($0) }
+                                hintLabel = "Smooth Glucose Value"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Smooth Glucose Value",
+                        miniHint: "Smooth CGM readings using Savitzky-Golay filtering.",
+                        verboseHint:
+                        VStack(alignment: .leading, spacing: 10) {
+                            Text("Default: OFF").bold()
+                            Text(
+                                "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
+                            )
+                            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."
+                            )
+                            Text(
+                                "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app."
+                            )
+                        }
+                    )
+                }
+                .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+                .onAppear(perform: configureView)
+                .navigationTitle("CGM")
+                .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
+                .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {
+                    switch state.cgmCurrent.type {
+                    case .enlite,
+                         .nightscout,
+                         .none,
+                         .simulator,
+                         .xdrip:
+
+                        CustomCGMOptionsView(
+                            resolver: self.resolver,
+                            state: state,
+                            cgmCurrent: state.cgmCurrent,
+                            deleteCGM: state.deleteCGM
+                        )
+
+                    case .plugin:
+                        if let fetchGlucoseManager = state.fetchGlucoseManager,
+                           let cgmManager = fetchGlucoseManager.cgmManager,
+                           state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
+                           state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
+                        {
+                            CGMSettingsView(
+                                cgmManager: cgmManager,
+                                bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                                unit: state.settingsManager.settings.units,
+                                completionDelegate: state
+                            )
+                        } else {
+                            CGMSetupView(
+                                CGMType: state.cgmCurrent,
+                                bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                                unit: state.settingsManager.settings.units,
+                                completionDelegate: state,
+                                setupDelegate: state,
+                                pluginCGMManager: self.state.pluginCGMManager
+                            )
+                        }
+                    }
+                }
+                .sheet(isPresented: $shouldDisplayHint) {
+                    SettingInputHintView(
+                        hintDetent: $hintDetent,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        hintLabel: hintLabel ?? "",
+                        hintText: AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Current CGM Models Supported:"
+                                )
+                                VStack(alignment: .leading) {
+                                    Text("• Dexcom G5")
+                                    Text("• Dexcom G6 / ONE")
+                                    Text("• Dexcom G7 / ONE+")
+                                    Text("• Dexcom Share")
+                                    Text("• Freestyle Libre")
+                                    Text("• Freestyle Libre Demo")
+                                    Text("• Glucose Simulator")
+                                    Text("• Medtronic Enlite")
+                                    Text("• Nightscout")
+                                    Text("• xDrip4iOS")
+                                }
+                                Text(
+                                    "Note: The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
+                                )
+                            }
+                        ),
+                        sheetTitle: "Help"
+                    )
+                }
+                .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
+                    cgmSelectionButtons
+                } message: {
+                    Text("Select CGM Model")
+                }
+            }
+        }
+    }
+}

+ 1 - 1
Trio/Sources/Modules/CGM/View/CGMSettingsView.swift

@@ -3,7 +3,7 @@ import LoopKitUI
 import SwiftUI
 import SwiftUI
 import UIKit
 import UIKit
 
 
-extension CGM {
+extension CGMSettings {
     struct CGMSettingsView: UIViewControllerRepresentable {
     struct CGMSettingsView: UIViewControllerRepresentable {
         let cgmManager: CGMManagerUI?
         let cgmManager: CGMManagerUI?
         let bluetoothManager: BluetoothStateManager
         let bluetoothManager: BluetoothStateManager

+ 2 - 2
Trio/Sources/Modules/CGM/View/CGMSetupView.swift

@@ -3,9 +3,9 @@ import LoopKitUI
 import SwiftUI
 import SwiftUI
 import UIKit
 import UIKit
 
 
-extension CGM {
+extension CGMSettings {
     struct CGMSetupView: UIViewControllerRepresentable {
     struct CGMSetupView: UIViewControllerRepresentable {
-        let CGMType: cgmName
+        let CGMType: CGMModel
         let bluetoothManager: BluetoothStateManager
         let bluetoothManager: BluetoothStateManager
         let unit: GlucoseUnits
         let unit: GlucoseUnits
         weak var completionDelegate: CompletionDelegate?
         weak var completionDelegate: CompletionDelegate?

+ 210 - 0
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -0,0 +1,210 @@
+import LoopKitUI
+import SwiftUI
+import Swinject
+
+extension CGMSettings {
+    struct CustomCGMOptionsView: BaseView {
+        let resolver: Resolver
+        @ObservedObject var state: CGMSettings.StateModel
+        let cgmCurrent: CGMModel
+        let deleteCGM: () -> Void
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+        @Environment(\.presentationMode) var presentationMode
+
+        @State private var shouldDisplayDeletionConfirmation: Bool = false
+
+        var body: some View {
+            NavigationView {
+                Form {
+                    if cgmCurrent.type != .none {
+                        if cgmCurrent.type == .nightscout {
+                            nightscoutSection
+                        } else {
+                            customCGMSection
+                        }
+
+                        if let appURL = cgmCurrent.type.appURL {
+                            Section {
+                                Button {
+                                    UIApplication.shared.open(appURL, options: [:]) { success in
+                                        if !success {
+                                            self.router.alertMessage
+                                                .send(MessageContent(
+                                                    content: "Unable to open the app",
+                                                    type: .warning
+                                                ))
+                                        }
+                                    }
+                                }
+
+                                label: {
+                                    Label(
+                                        "Open \(cgmCurrent.displayName)",
+                                        systemImage: "waveform.path.ecg.rectangle"
+                                    ).font(.title3)
+                                        .padding() }
+                                    .frame(maxWidth: .infinity, alignment: .center)
+                                    .buttonStyle(.bordered)
+                            }.listRowBackground(Color.clear)
+                        }
+                    }
+                }
+                .navigationTitle(cgmCurrent.displayName)
+                .navigationBarTitleDisplayMode(.inline)
+                .toolbar {
+                    /// proper positioning should be .leading
+                    /// LoopKit submodules set placement to .trailing; we'll keep it "proper" here
+                    ToolbarItem(placement: .topBarLeading) {
+                        Button("Close") {
+                            presentationMode.wrappedValue.dismiss()
+                        }
+                    }
+                }
+                .safeAreaInset(
+                    edge: .bottom,
+                    spacing: 30
+                ) {
+                    stickyDeleteButton
+                }
+                .scrollContentBackground(.hidden)
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .confirmationDialog("Delete CGM", isPresented: $shouldDisplayDeletionConfirmation) {
+                    Button(role: .destructive) {
+                        deleteCGM()
+                    } label: {
+                        Text("Delete \(cgmCurrent.displayName)")
+                            .font(.headline)
+                            .tint(.red)
+                    }
+                } message: { Text("Are you sure you want to delete \(cgmCurrent.displayName)?") }
+            }
+        }
+
+        var nightscoutSection: some View {
+            Group {
+                Section(
+                    header: Text("Configuration"),
+                    content: {
+                        VStack(alignment: .leading, spacing: 10) {
+                            Text("CGM is not used as heartbeat.").padding(.top)
+
+                            Text(
+                                state.url == nil ?
+                                    "To configure your CGM, tap the button below. In the form that opens, enter your Nightscout credentials to connect to your instance." :
+                                    "Tap the button below to open your Nightscout instance in your iPhone's default browser."
+                            ).font(.footnote)
+                                .foregroundColor(.secondary)
+                                .lineLimit(nil)
+                                .padding(.vertical)
+                        }
+
+                        if state.url == nil {
+                            NavigationLink(
+                                destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                                label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
+                            )
+                        }
+                    }
+                ).listRowBackground(Color.chart)
+
+                if let url = state.url {
+                    Section {
+                        Button {
+                            UIApplication.shared.open(url, options: [:]) { success in
+                                if !success {
+                                    self.router.alertMessage
+                                        .send(MessageContent(
+                                            content: "No URL available",
+                                            type: .warning
+                                        ))
+                                }
+                            }
+                        }
+                        label: {
+                            Label(
+                                "Open Nightscout",
+                                systemImage: "waveform.path.ecg.rectangle"
+                            ).font(.title3)
+                                .padding() }
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .buttonStyle(.bordered)
+                    }
+                    .listRowBackground(Color.clear)
+                }
+            }
+        }
+
+        var customCGMSection: some View {
+            Section(
+                header: Text("Configuration"),
+                content: {
+                    if cgmCurrent.type == .xdrip {
+                        VStack(alignment: .leading) {
+                            if let cgmTransmitterDeviceAddress = UserDefaults.standard
+                                .cgmTransmitterDeviceAddress
+                            {
+                                Text("CGM address :").padding(.top)
+                                Text(cgmTransmitterDeviceAddress)
+                            } else {
+                                Text("CGM is not used as heartbeat.").padding(.top)
+                            }
+
+                            HStack(alignment: .center) {
+                                Text(
+                                    "A heartbeat tells Trio to start a loop cycle. This is required for closed loop."
+                                )
+                                .font(.footnote)
+                                .foregroundColor(.secondary)
+                                .lineLimit(nil)
+                                Spacer()
+                            }.padding(.vertical)
+                        }
+                    } else if cgmCurrent.type == .simulator {
+                        Text(
+                            "Trio's glucose simulator does not offer any configuration. Its use is strictly for demonstration purposes only."
+                        )
+                    }
+
+                    if let link = cgmCurrent.type.externalLink {
+                        Button {
+                            UIApplication.shared.open(link, options: [:], completionHandler: nil)
+                        } label: {
+                            HStack {
+                                Text("About this source")
+                                Spacer()
+                                Image(systemName: "chevron.right")
+                            }
+                        }
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                }
+            ).listRowBackground(Color.chart)
+        }
+
+        var stickyDeleteButton: some View {
+            ZStack {
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Button(action: {
+                    shouldDisplayDeletionConfirmation.toggle()
+                }, label: {
+                    Text("Delete CGM")
+                        .frame(maxWidth: .infinity, maxHeight: .infinity)
+                        .padding(10)
+                })
+                    .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                    .background(Color(.systemRed))
+                    .tint(.white)
+                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                    .padding(5)
+            }
+        }
+    }
+}

+ 26 - 15
Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Observation
 import Observation
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
@@ -16,7 +17,8 @@ extension Calibrations {
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
-        private let context = CoreDataStack.shared.newTaskContext()
+        let backgroundContext = CoreDataStack.shared.newTaskContext()
+        private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
@@ -33,21 +35,27 @@ extension Calibrations {
             }
             }
         }
         }
 
 
-        private func fetchAndProcessGlucose() -> GlucoseStored? {
-            do {
-                debugPrint("Calibrations State Model: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
-                return try context.fetch(GlucoseStored.fetch(
-                    NSPredicate.predicateFor20MinAgo,
-                    ascending: false,
-                    fetchLimit: 1
-                )).first
-            } catch {
-                debugPrint("Calibrations State Model: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
-                return nil
+        /// - Returns: An array of NSManagedObjectIDs for glucose readings.
+        private func fetchGlucose() async -> [NSManagedObjectID] {
+            let results = await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: GlucoseStored.self,
+                onContext: backgroundContext,
+                predicate: NSPredicate.predicateFor20MinAgo,
+                key: "date",
+                ascending: false,
+                fetchLimit: 1 /// We only need the last value
+            )
+
+            return await backgroundContext.perform {
+                guard let glucoseResults = results as? [GlucoseStored] else {
+                    return []
+                }
+
+                return glucoseResults.map(\.objectID)
             }
             }
         }
         }
 
 
-        func addCalibration() {
+        @MainActor func addCalibration() async {
             defer {
             defer {
                 UIApplication.shared.endEditing()
                 UIApplication.shared.endEditing()
                 setupCalibrations()
                 setupCalibrations()
@@ -58,9 +66,12 @@ extension Calibrations {
                 glucose = newCalibration.asMgdL
                 glucose = newCalibration.asMgdL
             }
             }
 
 
-            if let lastGlucose = fetchAndProcessGlucose() {
-                let unfiltered = lastGlucose.glucose
+            let glucoseValuesIds = await fetchGlucose()
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
 
 
+            if let lastGlucose = glucoseObjects.first {
+                let unfiltered = lastGlucose.glucose
                 let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
                 let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
 
 
                 calibrationService.addCalibration(calibration)
                 calibrationService.addCalibration(calibration)

+ 3 - 1
Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -34,7 +34,9 @@ extension Calibrations {
                             Text(state.units.rawValue).foregroundColor(.secondary)
                             Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         }
                         Button {
                         Button {
-                            state.addCalibration()
+                            Task {
+                                await state.addCalibration()
+                            }
                         }
                         }
                         label: { Text("Add") }
                         label: { Text("Add") }
                             .disabled(state.newCalibration <= 0)
                             .disabled(state.newCalibration <= 0)

+ 0 - 5
Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -104,11 +104,6 @@ extension CarbRatioEditor {
             .navigationTitle("Carb Ratios")
             .navigationTitle("Carb Ratios")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                     Button(action: { state.add() }) {
                         HStack {
                         HStack {

+ 13 - 2
Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift

@@ -7,6 +7,7 @@ struct AddContactImageSheet: View {
 
 
     @ObservedObject var state: ContactImage.StateModel
     @ObservedObject var state: ContactImage.StateModel
 
 
+    @State private var contactName: String = ""
     @State private var hasHighContrast: Bool = true
     @State private var hasHighContrast: Bool = true
     @State private var ringWidth: ContactImageEntry.RingWidth = .regular
     @State private var ringWidth: ContactImageEntry.RingWidth = .regular
     @State private var ringGap: ContactImageEntry.RingGap = .small
     @State private var ringGap: ContactImageEntry.RingGap = .small
@@ -23,7 +24,7 @@ struct AddContactImageSheet: View {
     private var previewEntry: ContactImageEntry {
     private var previewEntry: ContactImageEntry {
         ContactImageEntry(
         ContactImageEntry(
             id: UUID(),
             id: UUID(),
-            name: "", // automatically set and populated
+            name: contactName, // automatically set and populated
             layout: layout,
             layout: layout,
             ring: ring,
             ring: ring,
             primary: primary,
             primary: primary,
@@ -66,6 +67,13 @@ struct AddContactImageSheet: View {
                 .padding(.bottom)
                 .padding(.bottom)
 
 
                 Form {
                 Form {
+                    Section(
+                        header: Text("Contact Name"),
+                        content: {
+                            TextField("Enter Name (Optional)", text: $contactName)
+                        }
+                    ).listRowBackground(Color.chart)
+
                     // Layout Section
                     // Layout Section
                     Section(header: Text("Style")) {
                     Section(header: Text("Style")) {
                         Picker("Layout", selection: $layout) {
                         Picker("Layout", selection: $layout) {
@@ -232,7 +240,10 @@ struct AddContactImageSheet: View {
     private func saveNewEntry() {
     private func saveNewEntry() {
         // Save the currently previewed entry
         // Save the currently previewed entry
         Task {
         Task {
-            await state.createAndSaveContactImage(entry: previewEntry, name: "Trio \(state.contactImageEntries.count + 1)")
+            await state.createAndSaveContactImage(
+                entry: previewEntry,
+                name: contactName.isEmpty ? "Trio \(state.contactImageEntries.count + 1)" : contactName
+            )
             dismiss()
             dismiss()
         }
         }
     }
     }

+ 7 - 1
Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift

@@ -19,7 +19,6 @@ struct ContactImageDetailView: View {
     var body: some View {
     var body: some View {
         VStack {
         VStack {
             HStack {
             HStack {
-                // TODO: - make this beautiful @Dan
                 Spacer()
                 Spacer()
                 ZStack {
                 ZStack {
                     Circle()
                     Circle()
@@ -41,6 +40,13 @@ struct ContactImageDetailView: View {
             .padding(.bottom)
             .padding(.bottom)
 
 
             Form {
             Form {
+                Section(
+                    header: Text("Contact Name"),
+                    content: {
+                        TextField("Enter Name (Optional)", text: $contactImageEntry.name)
+                    }
+                ).listRowBackground(Color.chart)
+
                 Section(header: Text("Style")) {
                 Section(header: Text("Style")) {
                     Picker("Layout", selection: $contactImageEntry.layout) {
                     Picker("Layout", selection: $contactImageEntry.layout) {
                         ForEach(ContactImageLayout.allCases, id: \.id) { layout in
                         ForEach(ContactImageLayout.allCases, id: \.id) { layout in

+ 2 - 0
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -313,6 +313,8 @@ extension DataTable {
                 )
                 )
 
 
                 await syncWithServices()
                 await syncWithServices()
+                // Perform a determine basal sync to update cob
+                await apsManager.determineBasalSync()
             }
             }
         }
         }
 
 

+ 1 - 1
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -171,7 +171,7 @@ struct CarbEntryEditorView: View {
                         selection: $editedDate,
                         selection: $editedDate,
                         displayedComponents: [.date, .hourAndMinute]
                         displayedComponents: [.date, .hourAndMinute]
                     )
                     )
-                }
+                }.listRowBackground(Color.chart)
             }
             }
             .safeAreaInset(
             .safeAreaInset(
                 edge: .bottom,
                 edge: .bottom,

+ 7 - 3
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -297,11 +297,15 @@ extension DynamicSettings {
                                 }
                                 }
                                 VStack(alignment: .leading, spacing: 5) {
                                 VStack(alignment: .leading, spacing: 5) {
                                     Text(
                                     Text(
-                                        "If your glucose target is 110 mg/dL, Trio will use a safety threshold of 75 mg/dL, unless you set Minimum Safety Threshold (mg/dL) to something > 75."
+                                        "If your glucose target is \(state.units == .mgdL ? "110" : 110.formattedAsMmolL) \(state.units.rawValue), Trio will use a safety threshold of \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue), unless you set Minimum Safety Threshold to something > \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue)."
+                                    )
+                                    Text(
+                                        "\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - 0.5 × (\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - \(state.units == .mgdL ? "40" : 40.formattedAsMmolL)) = \(state.units == .mgdL ? "75" : 75.formattedAsMmolL)"
                                     )
                                     )
-                                    Text("110 - 0.5 × (110 - 40) = 75")
                                 }
                                 }
-                                Text("This setting is limited to values between 60 - 120 mg/dL (3.3 - 6.6 mmol/L)")
+                                Text(
+                                    "This setting is limited to values between \(state.units == .mgdL ? "60" : 60.formattedAsMmolL) - \(state.units == .mgdL ? "120" : 120.formattedAsMmolL) \(state.units.rawValue)"
+                                )
                                 Text(
                                 Text(
                                     "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
                                     "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
                                 )
                                 )

+ 15 - 4
Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift

@@ -19,19 +19,30 @@ extension Home.StateModel {
                 // Ensure all values exist, otherwise set default values
                 // Ensure all values exist, otherwise set default values
                 guard let minGlucose = minGlucose, let maxGlucose = maxGlucose else {
                 guard let minGlucose = minGlucose, let maxGlucose = maxGlucose else {
                     Task {
                     Task {
-                        await self.updateChartBounds(minValue: 39, maxValue: 300)
+                        await self.updateChartBounds(minValue: 39, maxValue: 200)
                     }
                     }
                     return
                     return
                 }
                 }
 
 
-                // Adjust max forecast to be no more than 100 over max glucose
-                let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 100, maxGlucose + 100)
+                // Adjust max forecast to be no more than 50 over max glucose
+                let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 50, maxGlucose + 50)
                 let minOverall = min(minGlucose, minForecast ?? minGlucose)
                 let minOverall = min(minGlucose, minForecast ?? minGlucose)
                 let maxOverall = max(maxGlucose, adjustedMaxForecast)
                 let maxOverall = max(maxGlucose, adjustedMaxForecast)
 
 
+                var maxYValue = Decimal(200)
+                if maxOverall > 200, maxOverall <= 225 {
+                    maxYValue = Decimal(250)
+                } else if maxOverall > 225, maxOverall <= 275 {
+                    maxYValue = Decimal(300)
+                } else if maxOverall > 275, maxOverall <= 325 {
+                    maxYValue = Decimal(350)
+                } else if maxOverall > 325 {
+                    maxYValue = Decimal(400)
+                }
+
                 // Update the chart bounds on the main thread
                 // Update the chart bounds on the main thread
                 Task {
                 Task {
-                    await self.updateChartBounds(minValue: minOverall - 50, maxValue: maxOverall + 80)
+                    await self.updateChartBounds(minValue: minOverall, maxValue: maxYValue)
                 }
                 }
             }
             }
         }
         }

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

@@ -41,7 +41,7 @@ extension Home.StateModel {
             key: "deliverAt",
             key: "deliverAt",
             ascending: false,
             ascending: false,
             batchSize: 50,
             batchSize: 50,
-            propertiesToFetch: ["cob", "iob", "objectID"]
+            propertiesToFetch: ["cob", "iob", "deliverAt", "objectID"]
         )
         )
 
 
         return await determinationFetchContext.perform {
         return await determinationFetchContext.perform {

+ 166 - 28
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -1,3 +1,4 @@
+import CGMBLEKitUI
 import Combine
 import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
@@ -10,6 +11,7 @@ extension Home {
     @Observable final class StateModel: BaseStateModel<Provider> {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var broadcaster: Broadcaster!
         @ObservationIgnored @Injected() var broadcaster: Broadcaster!
         @ObservationIgnored @Injected() var apsManager: APSManager!
         @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var pluginCGMManager: PluginManager!
         @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
@@ -17,6 +19,11 @@ extension Home {
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
         @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+
+        var cgmStateModel: CGMSettings.StateModel {
+            CGMSettings.StateModel.shared
+        }
+
         private let timer = DispatchTimer(timeInterval: 5)
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         private(set) var filteredHours = 24
         var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
         var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
@@ -44,7 +51,8 @@ extension Home {
         var isExerciseModeActive: Bool = false
         var isExerciseModeActive: Bool = false
         var settingHalfBasalTarget: Decimal = 160
         var settingHalfBasalTarget: Decimal = 160
         var percentage: Int = 100
         var percentage: Int = 100
-        var setupPump = false
+        var shouldDisplayPumpSetupSheet = false
+        var shouldDisplayCGMSetupSheet = false
         var errorMessage: String?
         var errorMessage: String?
         var errorDate: Date?
         var errorDate: Date?
         var bolusProgress: Decimal?
         var bolusProgress: Decimal?
@@ -64,7 +72,6 @@ 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 timeZone: TimeZone?
         var hours: Int16 = 6
         var hours: Int16 = 6
         var totalBolus: Decimal = 0
         var totalBolus: Decimal = 0
         var isLoopStatusPresented: Bool = false
         var isLoopStatusPresented: Bool = false
@@ -91,7 +98,12 @@ extension Home {
         var isOverrideCancelled: Bool = false
         var isOverrideCancelled: Bool = false
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var pumpStatusHighlightMessage: String?
         var pumpStatusHighlightMessage: String?
+        var pumpStatusBadgeImage: UIImage?
+        var pumpStatusBadgeColor: Color?
         var cgmAvailable: Bool = false
         var cgmAvailable: Bool = false
+        var listOfCGM: [CGMModel] = []
+        var cgmCurrent = cgmDefaultModel
+
         var showCarbsRequiredBadge: Bool = true
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         var minForecast: [Int] = []
         var minForecast: [Int] = []
@@ -100,7 +112,7 @@ extension Home {
         var forecastDisplayType: ForecastDisplayType = .cone
         var forecastDisplayType: ForecastDisplayType = .cone
 
 
         var minYAxisValue: Decimal = 39
         var minYAxisValue: Decimal = 39
-        var maxYAxisValue: Decimal = 300
+        var maxYAxisValue: Decimal = 200
 
 
         var minValueCobChart: Decimal = 0
         var minValueCobChart: Decimal = 0
         var maxValueCobChart: Decimal = 20
         var maxValueCobChart: Decimal = 20
@@ -126,6 +138,10 @@ extension Home {
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
+        override init() {
+            super.init()
+        }
+
         override func subscribe() {
         override func subscribe() {
             coreDataPublisher =
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
                 changedObjectsOnManagedObjectContextDidSavePublisher()
@@ -145,6 +161,7 @@ extension Home {
                 // We need to initialize settings and observers first
                 // We need to initialize settings and observers first
                 await self.setupSettings()
                 await self.setupSettings()
                 await self.setupPumpSettings()
                 await self.setupPumpSettings()
+                await self.setupCGMSettings()
                 self.registerObservers()
                 self.registerObservers()
 
 
                 // The rest can be initialized concurrently
                 // The rest can be initialized concurrently
@@ -180,9 +197,6 @@ extension Home {
                         self.setupReservoir()
                         self.setupReservoir()
                     }
                     }
                     group.addTask {
                     group.addTask {
-                        self.setupCurrentPumpTimezone()
-                    }
-                    group.addTask {
                         self.setupOverrides()
                         self.setupOverrides()
                     }
                     }
                     group.addTask {
                     group.addTask {
@@ -238,6 +252,7 @@ extension Home {
                 self.setupInsulinArray()
                 self.setupInsulinArray()
                 self.setupLastBolus()
                 self.setupLastBolus()
                 self.displayPumpStatusHighlightMessage()
                 self.displayPumpStatusHighlightMessage()
+                self.displayPumpStatusBadge()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
             coreDataPublisher?.filterByEntityName("OpenAPS_Battery").sink { [weak self] _ in
             coreDataPublisher?.filterByEntityName("OpenAPS_Battery").sink { [weak self] _ in
@@ -330,10 +345,11 @@ extension Home {
                         self.battery = nil
                         self.battery = nil
                         self.pumpName = ""
                         self.pumpName = ""
                         self.pumpExpiresAtDate = nil
                         self.pumpExpiresAtDate = nil
-                        self.setupPump = false
+                        self.shouldDisplayPumpSetupSheet = false
                     } else {
                     } else {
                         self.setupReservoir()
                         self.setupReservoir()
                         self.displayPumpStatusHighlightMessage()
                         self.displayPumpStatusHighlightMessage()
+                        self.displayPumpStatusBadge()
                         self.setupBatteryArray()
                         self.setupBatteryArray()
                     }
                     }
                 }
                 }
@@ -364,7 +380,6 @@ extension Home {
             displayYgridLines = settingsManager.settings.yGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             thresholdLines = settingsManager.settings.rulerMarks
             thresholdLines = settingsManager.settings.rulerMarks
             totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
             totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
-            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             isExerciseModeActive = settingsManager.preferences.exerciseMode
@@ -374,9 +389,77 @@ extension Home {
             maxValue = settingsManager.preferences.autosensMax
             maxValue = settingsManager.preferences.autosensMax
         }
         }
 
 
+        @MainActor private func setupCGMSettings() async {
+            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
+
+            listOfCGM = (
+                CGMType.allCases.filter { $0 != CGMType.plugin }.map {
+                    CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+                } +
+                    pluginCGMManager.availableCGMManagers.map {
+                        CGMModel(
+                            id: $0.identifier,
+                            type: CGMType.plugin,
+                            displayName: $0.localizedTitle,
+                            subtitle: $0.localizedTitle
+                        )
+                    }
+            ).sorted(by: { lhs, rhs in
+                if lhs.displayName == "None" {
+                    return true
+                } else if rhs.displayName == "None" {
+                    return false
+                } else {
+                    return lhs.displayName < rhs.displayName
+                }
+            })
+
+            switch settingsManager.settings.cgm {
+            case .plugin:
+                if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
+                    cgmCurrent = CGMModel(
+                        id: settingsManager.settings.cgmPluginIdentifier,
+                        type: .plugin,
+                        displayName: cgmPluginInfo.displayName,
+                        subtitle: cgmPluginInfo.subtitle
+                    )
+                } else {
+                    // no more type of plugin available - fallback to default
+                    cgmCurrent = cgmDefaultModel
+                }
+            default:
+                cgmCurrent = CGMModel(
+                    id: settingsManager.settings.cgm.id,
+                    type: settingsManager.settings.cgm,
+                    displayName: settingsManager.settings.cgm.displayName,
+                    subtitle: settingsManager.settings.cgm.subtitle
+                )
+            }
+        }
+
         func addPump(_ type: PumpConfig.PumpType) {
         func addPump(_ type: PumpConfig.PumpType) {
             setupPumpType = type
             setupPumpType = type
-            setupPump = true
+            shouldDisplayPumpSetupSheet = true
+        }
+
+        func addCGM(cgm: CGMModel) {
+            cgmCurrent = cgm
+            switch cgmCurrent.type {
+            case .plugin:
+                shouldDisplayCGMSetupSheet = true
+            default:
+                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(CGMSetupCompletionNotifying())
+            }
+        }
+
+        func deleteCGM() {
+            shouldDisplayCGMSetupSheet = false
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
+                self.fetchGlucoseManager?.deleteGlucoseSource()
+                self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+            })
         }
         }
 
 
         /// Display the eventual status message provided by the manager of the pump
         /// Display the eventual status message provided by the manager of the pump
@@ -395,6 +478,22 @@ extension Home {
             }
             }
         }
         }
 
 
+        private func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,
+                   let image = statusBadge.image, !didDeactivate
+                {
+                    pumpStatusBadgeImage = image
+                    pumpStatusBadgeColor = statusBadge.state == .critical ? .critical : .warning
+
+                } else {
+                    pumpStatusBadgeImage = nil
+                    pumpStatusBadgeColor = nil
+                }
+            }
+        }
+
         func runLoop() {
         func runLoop() {
             provider.heartbeatNow()
             provider.heartbeatNow()
         }
         }
@@ -494,13 +593,6 @@ extension Home {
             }
             }
         }
         }
 
 
-        private func setupCurrentPumpTimezone() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.timeZone = self.provider.pumpTimeZone()
-            }
-        }
-
         private func getCurrentGlucoseTarget() async {
         private func getCurrentGlucoseTarget() async {
             let now = Date()
             let now = Date()
             let calendar = Calendar.current
             let calendar = Calendar.current
@@ -547,10 +639,6 @@ extension Home {
                 }
                 }
             }
             }
         }
         }
-
-        func openCGM() {
-            router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
-        }
     }
     }
 }
 }
 
 
@@ -562,7 +650,6 @@ extension Home.StateModel:
     BasalProfileObserver,
     BasalProfileObserver,
     BGTargetsObserver,
     BGTargetsObserver,
     PumpReservoirObserver,
     PumpReservoirObserver,
-    PumpTimeZoneObserver,
     PumpDeactivatedObserver
     PumpDeactivatedObserver
 {
 {
     func determinationDidUpdate(_: Determination) {
     func determinationDidUpdate(_: Determination) {
@@ -591,7 +678,11 @@ extension Home.StateModel:
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         displayPumpStatusHighlightMessage()
         displayPumpStatusHighlightMessage()
+        displayPumpStatusBadge()
         setupBatteryArray()
         setupBatteryArray()
+        Task {
+            await setupCGMSettings()
+        }
     }
     }
 
 
     func preferencesDidChange(_: Preferences) {
     func preferencesDidChange(_: Preferences) {
@@ -624,21 +715,53 @@ extension Home.StateModel:
     func pumpReservoirDidChange(_: Decimal) {
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
         setupReservoir()
         displayPumpStatusHighlightMessage()
         displayPumpStatusHighlightMessage()
+        displayPumpStatusBadge()
     }
     }
 
 
     func pumpDeactivatedDidChange() {
     func pumpDeactivatedDidChange() {
         displayPumpStatusHighlightMessage(true)
         displayPumpStatusHighlightMessage(true)
+        displayPumpStatusBadge(true)
         batteryFromPersistence = []
         batteryFromPersistence = []
     }
     }
-
-    func pumpTimeZoneDidChange(_: TimeZone) {
-        setupCurrentPumpTimezone()
-    }
 }
 }
 
 
 extension Home.StateModel: CompletionDelegate {
 extension Home.StateModel: CompletionDelegate {
-    func completionNotifyingDidComplete(_: CompletionNotifying) {
-        setupPump = false
+    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
+        debug(.service, "Completion fired by: \(type(of: notifying))")
+        shouldDisplayCGMSetupSheet = false
+
+        if notifying is CGMSetupCompletionNotifying || notifying is CGMDeletionCompletionNotifying ||
+            notifying is CGMManagerSettingsNavigationViewController || notifying is any SetupTableViewControllerDelegate ||
+            notifying is any CGMManagerOnboarding
+        {
+            if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                debug(.service, "CGMDeletionCompletionNotifying: CGM Deletion Completed")
+
+                cgmCurrent = cgmDefaultModel
+                settingsManager.settings.cgm = cgmDefaultModel.type
+                settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
+                fetchGlucoseManager.deleteGlucoseSource()
+            } else {
+                debug(.service, "CGMSetupCompletionNotifying: CGM Setup Completed")
+
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+
+                shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
+                    .type == .xdrip || cgmCurrent.type == .enlite
+            }
+
+            // update glucose source if required
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            // pump related handling
+            shouldDisplayPumpSetupSheet = false // hides sheet
+        }
     }
     }
 }
 }
 
 
@@ -655,6 +778,21 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
     }
     }
 
 
     func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
     func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
-        // TODO:
+        // nothing to do
+    }
+}
+
+extension Home.StateModel: CGMManagerOnboardingDelegate {
+    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
+        // update the glucose source
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
+    }
+
+    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
+        // nothing to do
     }
     }
 }
 }

+ 18 - 1
Trio/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift

@@ -80,7 +80,24 @@ extension MainChartView {
     }
     }
 
 
     func drawCOBIOBChart() -> some ChartContent {
     func drawCOBIOBChart() -> some ChartContent {
-        ForEach(state.enactedAndNonEnactedDeterminations) { item in
+        // Filter out duplicate entries by `deliverAt`,
+        // We sometimes get two determinations when editing carbs, one without the entry-to-be-edited and then another one after editing the entry.
+        // We are fetching determinations in descending order, so the first one is the latter determination (with correct amounts), so keeping the first one encountered.
+        var seenDates = Set<Date>()
+        let filteredDeterminations = state.enactedAndNonEnactedDeterminations.filter { item in
+            if let date = item.deliverAt {
+                if seenDates.contains(date) {
+                    // Already seen this date – filter it out.
+                    return false
+                } else {
+                    seenDates.insert(date)
+                    return true
+                }
+            }
+            return true
+        }
+
+        return ForEach(filteredDeterminations) { item in
 
 
             // MARK: - COB line and area mark
             // MARK: - COB line and area mark
 
 

+ 2 - 1
Trio/Sources/Modules/Home/View/Chart/ChartElements/ForecastView.swift

@@ -9,6 +9,7 @@ struct ForecastView: ChartContent {
     let units: GlucoseUnits
     let units: GlucoseUnits
     let maxValue: Decimal
     let maxValue: Decimal
     let forecastDisplayType: ForecastDisplayType
     let forecastDisplayType: ForecastDisplayType
+    let lastDeterminationDate: Date
 
 
     var body: some ChartContent {
     var body: some ChartContent {
         if forecastDisplayType == .lines {
         if forecastDisplayType == .lines {
@@ -19,7 +20,7 @@ struct ForecastView: ChartContent {
     }
     }
 
 
     private func timeForIndex(_ index: Int32) -> Date {
     private func timeForIndex(_ index: Int32) -> Date {
-        let currentTime = Date()
+        let currentTime = lastDeterminationDate
         let timeInterval = TimeInterval(index * 300)
         let timeInterval = TimeInterval(index * 300)
         return currentTime.addingTimeInterval(timeInterval)
         return currentTime.addingTimeInterval(timeInterval)
     }
     }

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

@@ -167,7 +167,8 @@ extension MainChartView {
                     maxForecast: state.maxForecast,
                     maxForecast: state.maxForecast,
                     units: state.units,
                     units: state.units,
                     maxValue: state.maxYAxisValue,
                     maxValue: state.maxYAxisValue,
-                    forecastDisplayType: state.forecastDisplayType
+                    forecastDisplayType: state.forecastDisplayType,
+                    lastDeterminationDate: state.determinationsFromPersistence.first?.deliverAt ?? .distantPast
                 )
                 )
 
 
                 /// show glucose value when hovering over it
                 /// show glucose value when hovering over it

+ 0 - 17
Trio/Sources/Modules/Home/View/Header/PumpView.swift

@@ -6,7 +6,6 @@ struct PumpView: View {
     let name: String
     let name: String
     let expiresAtDate: Date?
     let expiresAtDate: Date?
     let timerDate: Date
     let timerDate: Date
-    let timeZone: TimeZone?
     let pumpStatusHighlightMessage: String?
     let pumpStatusHighlightMessage: String?
     let battery: [OpenAPS_Battery]
     let battery: [OpenAPS_Battery]
 
 
@@ -68,22 +67,6 @@ struct PumpView: View {
                         Capsule()
                         Capsule()
                             .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
                             .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
                     )
                     )
-
-                    if let timeZone = timeZone, timeZone.secondsFromGMT() != TimeZone.current.secondsFromGMT() {
-                        HStack {
-                            Image(systemName: "clock.badge.exclamationmark.fill")
-                                .font(.callout)
-                                .symbolRenderingMode(.palette)
-                                .foregroundStyle(.red, Color(.warning))
-
-                            Text("Timezone")
-                                .font(.callout)
-                                .fontWeight(.bold)
-                                .fontDesign(.rounded)
-                                .foregroundStyle(.red)
-                        }
-                        .padding(.leading, 12)
-                    }
                 }
                 }
 
 
                 if (battery.first?.display) != nil, let shouldBatteryDisplay = battery.first?.display, shouldBatteryDisplay {
                 if (battery.first?.display) != nil, let shouldBatteryDisplay = battery.first?.display, shouldBatteryDisplay {

+ 100 - 12
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -32,6 +32,7 @@ extension Home {
         @State var showTreatments = false
         @State var showTreatments = false
         @State var selectedTab: Int = 0
         @State var selectedTab: Int = 0
         @State var showPumpSelection: Bool = false
         @State var showPumpSelection: Bool = false
+        @State var showCGMSelection: Bool = false
         @State var notificationsDisabled = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
         @State var timeButtons: [TimePicker] = [
             TimePicker(active: false, hours: 4),
             TimePicker(active: false, hours: 4),
@@ -80,6 +81,41 @@ extension Home {
             }
             }
         }
         }
 
 
+        @ViewBuilder func pumpTimezoneView(_ badgeImage: UIImage, _ badgeColor: Color) -> some View {
+            HStack {
+                Image(uiImage: badgeImage.withRenderingMode(.alwaysTemplate))
+                    .font(.system(size: 14))
+                    .colorMultiply(badgeColor)
+                Text(NSLocalizedString("Time Change Detected", comment: ""))
+                    .bold()
+                    .font(.system(size: 14))
+                    .foregroundStyle(badgeColor)
+            }
+            .onTapGesture {
+                if state.pumpDisplayState != nil {
+                    // sends user to pump settings
+                    state.shouldDisplayPumpSetupSheet.toggle()
+                }
+            }
+            .frame(maxWidth: .infinity, alignment: .center)
+            .padding(.vertical, 5)
+            .padding(.horizontal, 10)
+            .overlay(
+                Capsule()
+                    .stroke(badgeColor.opacity(0.4), lineWidth: 2)
+            )
+        }
+
+        var cgmSelectionButtons: some View {
+            ForEach(cgmOptions, id: \.name) { option in
+                if let cgm = state.listOfCGM.first(where: option.predicate) {
+                    Button(option.name) {
+                        state.addCGM(cgm: cgm)
+                    }
+                }
+            }
+        }
+
         var glucoseView: some View {
         var glucoseView: some View {
             CurrentGlucoseView(
             CurrentGlucoseView(
                 timerDate: state.timerDate,
                 timerDate: state.timerDate,
@@ -93,7 +129,11 @@ extension Home {
                 glucose: state.latestTwoGlucoseValues
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
             ).scaleEffect(0.9)
                 .onTapGesture {
                 .onTapGesture {
-                    state.openCGM()
+                    if !state.cgmAvailable {
+                        showCGMSelection.toggle()
+                    } else {
+                        state.shouldDisplayCGMSetupSheet.toggle()
+                    }
                 }
                 }
                 .onLongPressGesture {
                 .onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
@@ -108,7 +148,6 @@ extension Home {
                 name: state.pumpName,
                 name: state.pumpName,
                 expiresAtDate: state.pumpExpiresAtDate,
                 expiresAtDate: state.pumpExpiresAtDate,
                 timerDate: state.timerDate,
                 timerDate: state.timerDate,
-                timeZone: state.timeZone,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 battery: state.batteryFromPersistence
                 battery: state.batteryFromPersistence
             )
             )
@@ -118,7 +157,7 @@ extension Home {
                     showPumpSelection.toggle()
                     showPumpSelection.toggle()
                 } else {
                 } else {
                     // sends user to pump settings
                     // sends user to pump settings
-                    state.setupPump.toggle()
+                    state.shouldDisplayPumpSetupSheet.toggle()
                 }
                 }
             }
             }
         }
         }
@@ -839,12 +878,17 @@ extension Home {
                         pumpView
                         pumpView
                         Spacer()
                         Spacer()
                     }.padding(.leading, 20)
                     }.padding(.leading, 20)
-                }.padding(.top, 10)
-                    .safeAreaInset(edge: .top, spacing: 0) {
-                        if notificationsDisabled {
-                            alertSafetyNotificationsView(geo: geo)
-                        }
+                }
+                .padding(.top, 10)
+                .safeAreaInset(edge: .top, spacing: 0) {
+                    if notificationsDisabled {
+                        alertSafetyNotificationsView(geo: geo)
                     }
                     }
+                    if let badgeImage = state.pumpStatusBadgeImage, let badgeColor = state.pumpStatusBadgeColor {
+                        pumpTimezoneView(badgeImage, badgeColor)
+                            .padding(.horizontal, 20)
+                    }
+                }
 
 
                 mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
                 mealPanel(geo).padding(.top, UIDevice.adjustPadding(min: nil, max: 30))
                     .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
                     .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 20))
@@ -872,7 +916,7 @@ extension Home {
                         iconString: "info",
                         iconString: "info",
                         action: { state.isLegendPresented.toggle() }
                         action: { state.isLegendPresented.toggle() }
                     )
                     )
-                }.padding([.horizontal, .top, .bottom])
+                }.padding([.horizontal, .bottom])
 
 
                 if let progress = state.bolusProgress {
                 if let progress = state.bolusProgress {
                     bolusView(geo: geo, progress)
                     bolusView(geo: geo, progress)
@@ -914,6 +958,10 @@ extension Home {
             .sheet(isPresented: $state.isLoopStatusPresented) {
             .sheet(isPresented: $state.isLoopStatusPresented) {
                 LoopStatusView(state: state)
                 LoopStatusView(state: state)
             }
             }
+            .sheet(isPresented: $state.isLegendPresented) {
+                ChartLegendView(state: state)
+            }
+            // PUMP RELATED
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }
                 Button("Medtronic") { state.addPump(.minimed) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
@@ -921,7 +969,7 @@ extension Home {
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
                 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.setupPump) {
+            .sheet(isPresented: $state.shouldDisplayPumpSetupSheet) {
                 if let pumpManager = state.provider.apsManager.pumpManager {
                 if let pumpManager = state.provider.apsManager.pumpManager {
                     PumpConfig.PumpSettingsView(
                     PumpConfig.PumpSettingsView(
                         pumpManager: pumpManager,
                         pumpManager: pumpManager,
@@ -939,8 +987,48 @@ extension Home {
                     )
                     )
                 }
                 }
             }
             }
-            .sheet(isPresented: $state.isLegendPresented) {
-                ChartLegendView(state: state)
+            // CGM RELATED
+            .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
+                cgmSelectionButtons
+            } message: {
+                Text("Select CGM Model")
+            }
+            .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {
+                switch state.cgmCurrent.type {
+                case .enlite,
+                     .nightscout,
+                     .none,
+                     .simulator,
+                     .xdrip:
+                    CGMSettings.CustomCGMOptionsView(
+                        resolver: self.resolver,
+                        state: state.cgmStateModel,
+                        cgmCurrent: state.cgmCurrent,
+                        deleteCGM: state.deleteCGM
+                    )
+                case .plugin:
+                    if let fetchGlucoseManager = state.fetchGlucoseManager,
+                       let cgmManager = fetchGlucoseManager.cgmManager,
+                       state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
+                       state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
+                    {
+                        CGMSettings.CGMSettingsView(
+                            cgmManager: cgmManager,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            unit: state.settingsManager.settings.units,
+                            completionDelegate: state
+                        )
+                    } else {
+                        CGMSettings.CGMSetupView(
+                            CGMType: state.cgmCurrent,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            unit: state.settingsManager.settings.units,
+                            completionDelegate: state,
+                            setupDelegate: state,
+                            pluginCGMManager: self.state.pluginCGMManager
+                        )
+                    }
+                }
             }
             }
         }
         }
 
 

+ 0 - 5
Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -102,11 +102,6 @@ extension ISFEditor {
             .navigationTitle("Insulin Sensitivities")
             .navigationTitle("Insulin Sensitivities")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                     Button(action: { state.add() }) {
                         HStack {
                         HStack {

+ 0 - 15
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift

@@ -80,21 +80,6 @@ struct NightscoutConnectView: View {
                 }
                 }
                 .listRowBackground(Color.clear)
                 .listRowBackground(Color.clear)
             }
             }
-
-            // TODO: Find out if this is still required or needed ?!
-//            Section {
-//                Toggle("Use local glucose server", isOn: $state.useLocalSource)
-//                HStack {
-//                    Text("Port")
-//                    TextFieldWithToolBar(
-//                        text: $state.localPort,
-//                        placeholder: "",
-//                        keyboardType: .numberPad,
-//                        numberFormatter: portFormatter,
-//                        allowDecimalSeparator: false
-//                    )
-//                }
-//            } header: { Text("Local glucose source") }.listRowBackground(Color.chart)
         }
         }
         .listSectionSpacing(sectionSpacing)
         .listSectionSpacing(sectionSpacing)
         .navigationTitle("Connect")
         .navigationTitle("Connect")

+ 4 - 3
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -29,10 +29,12 @@ extension PumpConfig {
                                     state.setupPump = true
                                     state.setupPump = true
                                 } label: {
                                 } label: {
                                     HStack {
                                     HStack {
-                                        Image(uiImage: pumpState.image ?? UIImage()).padding()
+                                        Image(uiImage: pumpState.image ?? UIImage())
                                         Text(pumpState.name)
                                         Text(pumpState.name)
                                     }
                                     }
-                                }
+                                    .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
+                                    .font(.title2)
+                                }.padding()
                                 if state.alertNotAck {
                                 if state.alertNotAck {
                                     Spacer()
                                     Spacer()
                                     Button("Acknowledge all alerts") { state.ack() }
                                     Button("Acknowledge all alerts") { state.ack() }
@@ -70,7 +72,6 @@ extension PumpConfig {
                             }
                             }
                         }
                         }
                     )
                     )
-                    .padding(.top)
                     .listRowBackground(Color.chart)
                     .listRowBackground(Color.chart)
                 }
                 }
                 .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
                 .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))

+ 0 - 5
Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -86,11 +86,6 @@ extension TargetsEditor {
             .navigationTitle("Glucose Targets")
             .navigationTitle("Glucose Targets")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                     Button(action: { state.add() }) {
                         HStack {
                         HStack {

+ 10 - 31
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -380,26 +380,6 @@ extension Treatments {
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
         @MainActor func calculateInsulin() async -> Decimal {
         @MainActor func calculateInsulin() async -> Decimal {
-//            let input = CalculationInput(
-//                carbs: carbs,
-//                currentBG: currentBG,
-//                deltaBG: deltaBG,
-//                target: target,
-//                isf: isf,
-//                carbRatio: carbRatio,
-//                iob: iob,
-//                cob: cob,
-//                useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
-//                fattyMealFactor: fattyMealFactor,
-//                useSuperBolus: useSuperBolus,
-//                sweetMealFactor: sweetMealFactor,
-//                basal: basal,
-//                fraction: fraction,
-//                maxBolus: maxBolus
-//            )
-//
-//            let result = await bolusCalculationManager.calculateInsulin(input: input)
-
             let result = await bolusCalculationManager.handleBolusCalculation(
             let result = await bolusCalculationManager.handleBolusCalculation(
                 carbs: carbs,
                 carbs: carbs,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
@@ -432,19 +412,17 @@ extension Treatments {
                 let isFatPresent = fat > 0
                 let isFatPresent = fat > 0
                 let isProteinPresent = protein > 0
                 let isProteinPresent = protein > 0
 
 
+                if isCarbsPresent || isFatPresent || isProteinPresent {
+                    await saveMeal()
+                }
+
                 if isInsulinGiven {
                 if isInsulinGiven {
                     try await handleInsulin(isExternal: externalInsulin)
                     try await handleInsulin(isExternal: externalInsulin)
-                } else if isCarbsPresent || isFatPresent || isProteinPresent {
-                    await MainActor.run {
-                        self.waitForSuggestion = true
-                    }
                 } else {
                 } else {
                     hideModal()
                     hideModal()
                     return
                     return
                 }
                 }
 
 
-                await saveMeal()
-
                 // If glucose data is stale end the custom loading animation by hiding the modal
                 // If glucose data is stale end the custom loading animation by hiding the modal
                 // Get date on Main thread
                 // Get date on Main thread
                 let date = await MainActor.run {
                 let date = await MainActor.run {
@@ -560,11 +538,12 @@ extension Treatments {
             )]
             )]
             await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
             await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
 
 
-            if carbs > 0 || fat > 0 || protein > 0 {
-                // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
-                if amount <= 0 {
-                    await apsManager.determineBasalSync()
+            // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
+            if amount <= 0 {
+                await MainActor.run {
+                    self.waitForSuggestion = true
                 }
                 }
+                await apsManager.determineBasalSync()
             }
             }
         }
         }
 
 
@@ -809,7 +788,7 @@ extension Treatments.StateModel {
         } else {
         } else {
             simulatedDetermination = await Task { [self] in
             simulatedDetermination = await Task { [self] in
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
-                return await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
+                return await apsManager.simulateDetermineBasal(simulatedCarbsAmount: carbs, simulatedBolusAmount: amount)
             }.value
             }.value
         }
         }
 
 

+ 1 - 4
Trio/Sources/Router/Screen.swift

@@ -19,7 +19,6 @@ enum Screen: Identifiable, Hashable {
     case manualTempBasal
     case manualTempBasal
     case dataTable
     case dataTable
     case cgm
     case cgm
-    case cgmDirect
     case healthkit
     case healthkit
     case glucoseNotificationSettings
     case glucoseNotificationSettings
     case mealSettings
     case mealSettings
@@ -89,9 +88,7 @@ extension Screen {
         case .dataTable:
         case .dataTable:
             DataTable.RootView(resolver: resolver)
             DataTable.RootView(resolver: resolver)
         case .cgm:
         case .cgm:
-            CGM.RootView(resolver: resolver, displayClose: false)
-        case .cgmDirect:
-            CGM.RootView(resolver: resolver, displayClose: true)
+            CGMSettings.RootView(resolver: resolver, displayClose: false)
         case .healthkit:
         case .healthkit:
             AppleHealthKit.RootView(resolver: resolver)
             AppleHealthKit.RootView(resolver: resolver)
         case .glucoseNotificationSettings:
         case .glucoseNotificationSettings:

+ 0 - 3
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -769,9 +769,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                    isf > 0, cr > 0
                    isf > 0, cr > 0
                 {
                 {
                     carbsHr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
                     carbsHr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
-                    if settingsManager.settings.units == .mmolL {
-                        carbsHr *= GlucoseUnits.exchangeRate
-                    }
                     carbsHr = Decimal(round(Double(carbsHr) * 10.0)) / 10
                     carbsHr = Decimal(round(Double(carbsHr) * 10.0)) / 10
                 }
                 }
 
 

+ 27 - 0
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -253,6 +253,31 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             }
             }
             .sorted { $0.date < $1.date }
             .sorted { $0.date < $1.date }
 
 
+            // Set axis domain: min and max Y-axis values
+            // Apply unit parsing conditionally, if user uses mmol/L
+            let maxGlucoseValue = Decimal(glucoseObjects.map { Int($0.glucose) }.max() ?? 200)
+            var maxYValue = Decimal(200)
+
+            if maxGlucoseValue > maxYValue, maxGlucoseValue <= 225 {
+                maxYValue = Decimal(250)
+            } else if maxGlucoseValue > 225, maxGlucoseValue <= 275 {
+                maxYValue = Decimal(300)
+            } else if maxGlucoseValue > 275, maxGlucoseValue <= 325 {
+                maxYValue = Decimal(350)
+            } else if maxGlucoseValue > 325 {
+                maxYValue = Decimal(400)
+            }
+
+            if self.units == .mmolL {
+                maxYValue = Double(truncating: maxYValue as NSNumber).asMmolL
+            }
+            watchState.maxYAxisValue = maxYValue
+
+            if self.units == .mmolL {
+                let minYValue = Double(truncating: watchState.minYAxisValue as NSNumber).asMmolL
+                watchState.minYAxisValue = minYValue
+            }
+
             // Convert direction to trend string
             // Convert direction to trend string
             watchState.trend = latestGlucose.direction
             watchState.trend = latestGlucose.direction
 
 
@@ -386,6 +411,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     "color": value.color
                     "color": value.color
                 ]
                 ]
             },
             },
+            WatchMessageKeys.minYAxisValue: state.minYAxisValue,
+            WatchMessageKeys.maxYAxisValue: state.maxYAxisValue,
             WatchMessageKeys.overridePresets: state.overridePresets.map { preset in
             WatchMessageKeys.overridePresets: state.overridePresets.map { preset in
                 [
                 [
                     "name": preset.name,
                     "name": preset.name,

+ 1 - 0
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -167,6 +167,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
 
 
     func cancelTempTarget() async {
     func cancelTempTarget() async {
         await disableAllActiveTempTargets(createTempTargetRunEntry: true, shouldStartBackgroundTask: true)
         await disableAllActiveTempTargets(createTempTargetRunEntry: true, shouldStartBackgroundTask: true)
+        tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
     }
     }
 
 
     @MainActor func disableAllActiveTempTargets(
     @MainActor func disableAllActiveTempTargets(