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

Merge branch 'dev' of github.com:nightscout/Trio into oref-swift-sync-dev-2025-12-08

Deniz Cengiz 5 месяцев назад
Родитель
Сommit
95c1a57e3e
46 измененных файлов с 619 добавлено и 353 удалено
  1. 1 1
      CGMBLEKit
  2. 1 1
      Config.xcconfig
  3. 1 1
      DanaKit
  4. 1 1
      G7SensorKit
  5. 1 1
      LibreTransmitter
  6. 1 1
      LoopKit
  7. 4 0
      Model/Classes+Properties/DeletedGlucoseStored+CoreDataClass.swift
  8. 14 0
      Model/Classes+Properties/DeletedGlucoseStored+CoreDataProperties.swift
  9. 9 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  10. 1 1
      OmniBLE
  11. 1 1
      OmniKit
  12. 1 1
      RileyLinkKit
  13. 1 1
      TidepoolService
  14. 9 5
      Trio Watch App Extension/Views/TrioMainWatchView.swift
  15. 2 1
      Trio Watch App Extension/WatchState.swift
  16. 16 8
      Trio.xcodeproj/project.pbxproj
  17. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  18. 25 9
      Trio/Sources/APS/APSManager.swift
  19. 29 5
      Trio/Sources/APS/DeviceDataManager.swift
  20. 0 10
      Trio/Sources/APS/Extensions/OmniBLEPumpManagerExtensions.swift
  21. 0 10
      Trio/Sources/APS/Extensions/OmniPodManagerExtensions.swift
  22. 9 11
      Trio/Sources/APS/FetchGlucoseManager.swift
  23. 65 10
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  24. 45 34
      Trio/Sources/APS/Storage/TDDStorage.swift
  25. 3 0
      Trio/Sources/Application/TrioApp.swift
  26. 8 0
      Trio/Sources/Helpers/Formatters.swift
  27. 2 0
      Trio/Sources/Models/NightscoutStatus.swift
  28. 1 1
      Trio/Sources/Models/Preferences.swift
  29. 6 6
      Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  30. 3 3
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  31. 2 6
      Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  32. 14 4
      Trio/Sources/Modules/Home/HomeStateModel.swift
  33. 3 2
      Trio/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift
  34. 6 1
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  35. 73 35
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  36. 1 1
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  37. 3 3
      Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift
  38. 10 5
      Trio/Sources/Modules/Treatments/View/PopupView.swift
  39. 1 1
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  40. 90 0
      Trio/Sources/Services/Network/Nightscout/BaseNightscoutManager+Subscribers.swift
  41. 96 166
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  42. 42 0
      Trio/Sources/Services/Network/Nightscout/NightscoutUploadPipeline.swift
  43. 11 0
      TrioTests/CoreDataTests/GlucoseStorageTests.swift
  44. 1 1
      dexcom-share-client-swift
  45. 2 2
      fastlane/testflight.md
  46. 3 1
      oref0_source_version.txt

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit 1cc9e9e7627cf8fb76ccdb015dd6991196038e31
+Subproject commit 26fa00bed8c2f5e4b52ecb3241b422d058117c2c

+ 1 - 1
Config.xcconfig

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

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 084de69f69b1b17c92b595b4d5afeaed5b5d1e55
+Subproject commit 299331d4e540a0e7d1a74c30ddbb5be1d68892e8

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 9fa27889f0b216cbe0a23844e888de6698793b63
+Subproject commit 43f55ad8e1227fa6b4bec25d152726c56c0ffb0c

+ 1 - 1
LibreTransmitter

@@ -1 +1 @@
-Subproject commit 1950f1fec2a0e9f256c1be6e5bafd06ff79d3144
+Subproject commit 25c31bae22082caaa6823179010129912d6c8f8f

+ 1 - 1
LoopKit

@@ -1 +1 @@
-Subproject commit ddee20aca806f7635b8421617a675ddbd9c6d924
+Subproject commit ce07c0993b1038f6f60ea5b6db7c23da0be3fee6

+ 4 - 0
Model/Classes+Properties/DeletedGlucoseStored+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(DeletedGlucoseStored) public class DeletedGlucoseStored: NSManagedObject {}

+ 14 - 0
Model/Classes+Properties/DeletedGlucoseStored+CoreDataProperties.swift

@@ -0,0 +1,14 @@
+import CoreData
+import Foundation
+
+public extension DeletedGlucoseStored {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<GlucoseStored> {
+        NSFetchRequest<GlucoseStored>(entityName: "DeletedGlucoseStored")
+    }
+
+    @NSManaged var date: Date
+    @NSManaged var glucose: Int16
+    @NSManaged var isManualGlucoseEntry: Bool
+}
+
+extension DeletedGlucoseStored: Identifiable {}

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

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788" systemVersion="24G84" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -42,6 +42,14 @@
         <attribute name="ringWidth" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="ringWidth" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="top" optional="YES" attributeType="String"/>
         <attribute name="top" optional="YES" attributeType="String"/>
     </entity>
     </entity>
+    <entity name="DeletedGlucoseStored" representedClassName="DeletedGlucoseStored" syncable="YES">
+        <attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="glucose" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="isManualGlucoseEntry" attributeType="Boolean" usesScalarValueType="YES"/>
+        <fetchIndex name="byDate">
+            <fetchIndexElement property="date" type="Binary" order="ascending"/>
+        </fetchIndex>
+    </entity>
     <entity name="Forecast" representedClassName="Forecast" syncable="YES">
     <entity name="Forecast" representedClassName="Forecast" syncable="YES">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit e4378ba744a46c5f06f9507eabceb4072c058992
+Subproject commit d8375ebf242e0d0e02ace7a03d9e1632557de38e

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 1be14fcc27f22258cf8daa0355ac70c89737c0cc
+Subproject commit 1a73635568750289ac4d2f702b6bf191efbdda9f

+ 1 - 1
RileyLinkKit

@@ -1 +1 @@
-Subproject commit b280e8b9b7e75674b763f3ebf96d8b21dddcf80a
+Subproject commit c818fa8c90c0c98a4ba26cd18dacfeed01cc2bd5

+ 1 - 1
TidepoolService

@@ -1 +1 @@
-Subproject commit 59b0cd9384d180c7cccaf2cd2416fa2592a0ce45
+Subproject commit 84cab9b60e65b4aa814b0e12024a5e068ca65bfd

+ 9 - 5
Trio Watch App Extension/Views/TrioMainWatchView.swift

@@ -111,22 +111,26 @@ struct TrioMainWatchView: View {
             }
             }
             .toolbar {
             .toolbar {
                 ToolbarItem(placement: .topBarLeading) {
                 ToolbarItem(placement: .topBarLeading) {
-                    HStack {
+                    VStack {
                         Image(systemName: "syringe.fill")
                         Image(systemName: "syringe.fill")
                             .foregroundStyle(Color.insulin)
                             .foregroundStyle(Color.insulin)
 
 
                         Text(isWatchStateDated || isSessionUnreachable ? "--" : state.iob ?? "--")
                         Text(isWatchStateDated || isSessionUnreachable ? "--" : state.iob ?? "--")
                             .foregroundStyle(isWatchStateDated ? Color.secondary : Color.white)
                             .foregroundStyle(isWatchStateDated ? Color.secondary : Color.white)
+                            .frame(alignment: .leading)
+                            .minimumScaleFactor(0.5)
                     }.font(.caption2)
                     }.font(.caption2)
                 }
                 }
 
 
                 ToolbarItem(placement: .topBarTrailing) {
                 ToolbarItem(placement: .topBarTrailing) {
-                    HStack {
-                        Text(isWatchStateDated || isSessionUnreachable ? "--" : state.cob ?? "--")
-                            .foregroundStyle(isWatchStateDated || isSessionUnreachable ? Color.secondary : Color.white)
-
+                    VStack {
                         Image(systemName: "fork.knife")
                         Image(systemName: "fork.knife")
                             .foregroundStyle(Color.orange)
                             .foregroundStyle(Color.orange)
+
+                        Text(isWatchStateDated || isSessionUnreachable ? "--" : state.cob ?? "--")
+                            .foregroundStyle(isWatchStateDated || isSessionUnreachable ? Color.secondary : Color.white)
+                            .frame(alignment: .trailing)
+                            .minimumScaleFactor(0.5)
                     }.font(.caption2)
                     }.font(.caption2)
                 }
                 }
 
 

+ 2 - 1
Trio Watch App Extension/WatchState.swift

@@ -563,7 +563,8 @@ import WatchConnectivity
 
 
         if let bolusIncrement = message[WatchMessageKeys.bolusIncrement] {
         if let bolusIncrement = message[WatchMessageKeys.bolusIncrement] {
             if let decimalValue = (bolusIncrement as? NSNumber)?.decimalValue {
             if let decimalValue = (bolusIncrement as? NSNumber)?.decimalValue {
-                self.bolusIncrement = decimalValue
+                // limit minimum to 0.05 to avoid dealing with 0.025 increments
+                self.bolusIncrement = max(decimalValue, 0.05)
             }
             }
         }
         }
 
 

+ 16 - 8
Trio.xcodeproj/project.pbxproj

@@ -302,6 +302,8 @@
 		3B8B5D3C2DF523C000365ED3 /* AutosensJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8B5D3B2DF523B800365ED3 /* AutosensJsonTests.swift */; };
 		3B8B5D3C2DF523C000365ED3 /* AutosensJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8B5D3B2DF523B800365ED3 /* AutosensJsonTests.swift */; };
 		3B8B5D3E2DF5240C00365ED3 /* TimeZoneForTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8B5D3D2DF5240600365ED3 /* TimeZoneForTests.swift */; };
 		3B8B5D3E2DF5240C00365ED3 /* TimeZoneForTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8B5D3D2DF5240600365ED3 /* TimeZoneForTests.swift */; };
 		3B8B5D402DF52D0E00365ED3 /* deviationsUnsorted.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B8B5D3F2DF52D0700365ED3 /* deviationsUnsorted.json */; };
 		3B8B5D402DF52D0E00365ED3 /* deviationsUnsorted.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B8B5D3F2DF52D0700365ED3 /* deviationsUnsorted.json */; };
+		3B56079E2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B56079D2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift */; };
+		3B5607A02ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B56079F2ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift */; };
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
 		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
@@ -563,8 +565,6 @@
 		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 */; };
 		CE3EEF9A2D463717001944DD /* CustomCGMOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EEF992D46370A001944DD /* CustomCGMOptionsView.swift */; };
-		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
-		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
 		CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7950232997D81700FA576E /* CGMSettingsView.swift */; };
 		CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7950232997D81700FA576E /* CGMSettingsView.swift */; };
 		CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7950252998056D00FA576E /* CGMSetupView.swift */; };
 		CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7950252998056D00FA576E /* CGMSetupView.swift */; };
 		CE7CA34E2A064973004BE681 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3432A064973004BE681 /* AppShortcuts.swift */; };
 		CE7CA34E2A064973004BE681 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3432A064973004BE681 /* AppShortcuts.swift */; };
@@ -717,6 +717,8 @@
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
+		DD906BF42EA6AA0100262772 /* NightscoutUploadPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD906BF32EA6AA0100262772 /* NightscoutUploadPipeline.swift */; };
+		DD906BF62EA6AAE900262772 /* BaseNightscoutManager+Subscribers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD906BF52EA6AAE900262772 /* BaseNightscoutManager+Subscribers.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
@@ -1239,6 +1241,8 @@
 		3B8B5D3B2DF523B800365ED3 /* AutosensJsonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensJsonTests.swift; sourceTree = "<group>"; };
 		3B8B5D3B2DF523B800365ED3 /* AutosensJsonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensJsonTests.swift; sourceTree = "<group>"; };
 		3B8B5D3D2DF5240600365ED3 /* TimeZoneForTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneForTests.swift; sourceTree = "<group>"; };
 		3B8B5D3D2DF5240600365ED3 /* TimeZoneForTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneForTests.swift; sourceTree = "<group>"; };
 		3B8B5D3F2DF52D0700365ED3 /* deviationsUnsorted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = deviationsUnsorted.json; sourceTree = "<group>"; };
 		3B8B5D3F2DF52D0700365ED3 /* deviationsUnsorted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = deviationsUnsorted.json; sourceTree = "<group>"; };
+		3B56079D2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeletedGlucoseStored+CoreDataClass.swift"; sourceTree = "<group>"; };
+		3B56079F2ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeletedGlucoseStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
 		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
 		3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporterTests.swift; sourceTree = "<group>"; };
 		3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporterTests.swift; sourceTree = "<group>"; };
 		3B997DD12DC02AEF006B6BB2 /* glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = glucose.json; sourceTree = "<group>"; };
 		3B997DD12DC02AEF006B6BB2 /* glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = glucose.json; sourceTree = "<group>"; };
@@ -1503,8 +1507,6 @@
 		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>"; };
 		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>"; };
-		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>"; };
 		CE6B025628F350FF000C5502 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS9.1.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
 		CE6B025628F350FF000C5502 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS9.1.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
 		CE7950232997D81700FA576E /* CGMSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMSettingsView.swift; sourceTree = "<group>"; };
 		CE7950232997D81700FA576E /* CGMSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMSettingsView.swift; sourceTree = "<group>"; };
@@ -1657,6 +1659,8 @@
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
+		DD906BF32EA6AA0100262772 /* NightscoutUploadPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadPipeline.swift; sourceTree = "<group>"; };
+		DD906BF52EA6AAE900262772 /* BaseNightscoutManager+Subscribers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseNightscoutManager+Subscribers.swift"; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
@@ -2708,8 +2712,6 @@
 				3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */,
 				3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */,
 				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
 				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
-				CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */,
-				CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */,
 				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
 				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
 				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 			);
 			);
@@ -3956,8 +3958,10 @@
 		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD906BF32EA6AA0100262772 /* NightscoutUploadPipeline.swift */,
 				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
 				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
 				38FE826C25CC8461001FF17A /* NightscoutAPI.swift */,
 				38FE826C25CC8461001FF17A /* NightscoutAPI.swift */,
+				DD906BF52EA6AAE900262772 /* BaseNightscoutManager+Subscribers.swift */,
 			);
 			);
 			path = Nightscout;
 			path = Nightscout;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -4036,6 +4040,8 @@
 				DDE1793B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift */,
 				DDE1793B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift */,
 				DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */,
 				DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */,
 				DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */,
 				DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */,
+				3B56079D2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift */,
+				3B56079F2ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift */,
 				DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */,
 				DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */,
 				DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */,
 				DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */,
 				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
 				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
@@ -4638,6 +4644,7 @@
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */,
 				C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */,
+				DD906BF42EA6AA0100262772 /* NightscoutUploadPipeline.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
@@ -4834,7 +4841,6 @@
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				DD1745192C543B5700211FAC /* NotificationsView.swift in Sources */,
 				DD1745192C543B5700211FAC /* NotificationsView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
-				CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
@@ -4870,6 +4876,7 @@
 				3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */,
 				3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
+				3B5607A02ECD62AC00C723C1 /* DeletedGlucoseStored+CoreDataProperties.swift in Sources */,
 				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,
 				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
@@ -4962,6 +4969,7 @@
 				491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */,
 				491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */,
 				DD30B9CC2E062A7000DA677C /* ForecastResult.swift in Sources */,
 				DD30B9CC2E062A7000DA677C /* ForecastResult.swift in Sources */,
 				491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */,
 				491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */,
+				3B56079E2ECD62A800C723C1 /* DeletedGlucoseStored+CoreDataClass.swift in Sources */,
 				491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */,
 				491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */,
 				491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */,
 				491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
@@ -4975,6 +4983,7 @@
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
+				DD906BF62EA6AAE900262772 /* BaseNightscoutManager+Subscribers.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */,
 				DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */,
@@ -5095,7 +5104,6 @@
 				DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */,
 				DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */,
 				19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */,
 				19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */,
 				DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */,
 				DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */,
-				CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */,
 				DD5DC9F92CF3DAA900AB8703 /* RadioButton.swift in Sources */,
 				DD5DC9F92CF3DAA900AB8703 /* RadioButton.swift in Sources */,
 				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
 				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
 				CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */,
 				CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */,

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

@@ -1,5 +1,5 @@
 {
 {
-  "originHash" : "598841ae6fe892058ca678f5672f34299df2d62843330367c207648003263ccd",
+  "originHash" : "1e72c1cdf8ea5ec9fe527ebfab01ea55fca9e8651fe3252338fd3d4ea2cb327a",
   "pins" : [
   "pins" : [
     {
     {
       "identity" : "abseil-cpp-binary",
       "identity" : "abseil-cpp-binary",

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

@@ -19,6 +19,8 @@ protocol APSManager {
     var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
     var bolusProgress: CurrentValueSubject<Decimal?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     var isManualTempBasal: Bool { get }
+    var isScheduledBasal: Bool? { get }
+    var isSuspended: Bool { get }
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func determineBasal() async throws
     func determineBasal() async throws
     func determineBasalSync() async throws
     func determineBasalSync() async throws
@@ -76,7 +78,6 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
     @Injected() private var deviceDataManager: DeviceDataManager!
-    @Injected() private var nightscout: NightscoutManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var tddStorage: TDDStorage!
     @Injected() private var tddStorage: TDDStorage!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
@@ -105,6 +106,10 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
     @Persisted(key: "isManualTempBasal") var isManualTempBasal: Bool = false
 
 
+    @Persisted(key: "isScheduledBasal") var isScheduledBasal: Bool? = false
+
+    @Persisted(key: "isSuspended") var isSuspended: Bool = false
+
     let isLooping = CurrentValueSubject<Bool, Never>(false)
     let isLooping = CurrentValueSubject<Bool, Never>(false)
     let lastLoopDateSubject = PassthroughSubject<Date, Never>()
     let lastLoopDateSubject = PassthroughSubject<Date, Never>()
     let lastError = CurrentValueSubject<Error?, Never>(nil)
     let lastError = CurrentValueSubject<Error?, Never>(nil)
@@ -184,7 +189,21 @@ final class BaseAPSManager: APSManager, Injectable {
             }
             }
             .store(in: &lifetime)
             .store(in: &lifetime)
 
 
-        // manage a manual Temp Basal from OmniPod - Force loop() after stop a temp basal or finished
+        deviceDataManager.scheduledBasal
+            .receive(on: processQueue)
+            .sink { scheduledBasal in
+                self.isScheduledBasal = scheduledBasal
+            }
+            .store(in: &lifetime)
+
+        deviceDataManager.suspended
+            .receive(on: processQueue)
+            .sink { suspended in
+                self.isSuspended = suspended
+            }
+            .store(in: &lifetime)
+
+        // manage a manual Temp Basal from PumpManager - force loop() after manual temp basal is cancelled or finishes
         deviceDataManager.manualTempBasal
         deviceDataManager.manualTempBasal
             .receive(on: processQueue)
             .receive(on: processQueue)
             .sink { manualBasal in
             .sink { manualBasal in
@@ -219,13 +238,10 @@ final class BaseAPSManager: APSManager, Injectable {
                 // Execute loop logic
                 // Execute loop logic
                 try await self.executeLoop(loopStatRecord: &loopStatRecord)
                 try await self.executeLoop(loopStatRecord: &loopStatRecord)
 
 
-                // Upload data to Nightscout if available
-                if let nightscoutManager = self.nightscout {
-                    await nightscoutManager.uploadCarbs()
-                    await nightscoutManager.uploadPumpHistory()
-                    await nightscoutManager.uploadOverrides()
-                    await nightscoutManager.uploadTempTargets()
-                }
+                requestNightscoutUpload(
+                    [.carbs, .pumpHistory, .overrides, .tempTargets],
+                    source: "APSManager"
+                )
             } catch {
             } catch {
                 var updatedStats = loopStatRecord
                 var updatedStats = loopStatRecord
                 updatedStats.end = Date()
                 updatedStats.end = Date()

+ 29 - 5
Trio/Sources/APS/DeviceDataManager.swift

@@ -22,6 +22,8 @@ protocol DeviceDataManager: GlucoseSource {
     var recommendsLoop: PassthroughSubject<Void, Never> { get }
     var recommendsLoop: PassthroughSubject<Void, Never> { get }
     var bolusTrigger: PassthroughSubject<Bool, Never> { get }
     var bolusTrigger: PassthroughSubject<Bool, Never> { get }
     var manualTempBasal: PassthroughSubject<Bool, Never> { get }
     var manualTempBasal: PassthroughSubject<Bool, Never> { get }
+    var scheduledBasal: PassthroughSubject<Bool?, Never> { get }
+    var suspended: PassthroughSubject<Bool, Never> { get }
     var errorSubject: PassthroughSubject<Error, Never> { get }
     var errorSubject: PassthroughSubject<Error, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
@@ -68,6 +70,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     let errorSubject = PassthroughSubject<Error, Never>()
     let errorSubject = PassthroughSubject<Error, Never>()
     let pumpNewStatus = PassthroughSubject<Void, Never>()
     let pumpNewStatus = PassthroughSubject<Void, Never>()
     let manualTempBasal = PassthroughSubject<Bool, Never>()
     let manualTempBasal = PassthroughSubject<Bool, Never>()
+    let scheduledBasal = PassthroughSubject<Bool?, Never>()
+    let suspended = PassthroughSubject<Bool, Never>()
 
 
     private let router = TrioApp.resolver.resolve(Router.self)!
     private let router = TrioApp.resolver.resolve(Router.self)!
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
@@ -77,11 +81,15 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
 
     var pumpManager: PumpManagerUI? {
     var pumpManager: PumpManagerUI? {
         didSet {
         didSet {
-            pumpManager?.pumpManagerDelegate = self
-            pumpManager?.delegateQueue = processQueue
-            rawPumpManager = pumpManager?.rawValue
-            UserDefaults.standard.clearLegacyPumpManagerRawValue()
             if let pumpManager = pumpManager {
             if let pumpManager = pumpManager {
+                pumpManager.pumpManagerDelegate = self
+                pumpManager.delegateQueue = processQueue
+
+                /// Since the pump manager has been successfully instantiated from its saved state,
+                /// copy its rawValue to rawPumpManager which will be saved to persistant storage.
+                rawPumpManager = pumpManager.rawValue
+                UserDefaults.standard.clearLegacyPumpManagerRawValue()
+
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpName.send(pumpManager.localizedTitle)
                 pumpName.send(pumpManager.localizedTitle)
 
 
@@ -94,7 +102,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         )
                         )
                 )
                 )
                 modifiedPreferences
                 modifiedPreferences
-                    .bolusIncrement = bolusIncrement != 0.025 ? bolusIncrement : 0.1
+                    .bolusIncrement = bolusIncrement > 0 ? bolusIncrement : 0.1
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
 
 
                 if let omnipod = pumpManager as? OmnipodPumpManager {
                 if let omnipod = pumpManager as? OmnipodPumpManager {
@@ -411,6 +419,22 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             bolusTrigger.send(false)
             bolusTrigger.send(false)
         }
         }
 
 
+        switch status.basalDeliveryState {
+        case let .active(at):
+            if at == .distantPast {
+                scheduledBasal.send(nil) // pump is not currently available
+            } else {
+                suspended.send(false)
+                scheduledBasal.send(true)
+            }
+        case .suspended:
+            suspended.send(true)
+            scheduledBasal.send(false)
+        default:
+            suspended.send(false)
+            scheduledBasal.send(false)
+        }
+
         if status.insulinType != oldStatus.insulinType {
         if status.insulinType != oldStatus.insulinType {
             settingsManager.updateInsulinCurve(status.insulinType)
             settingsManager.updateInsulinCurve(status.insulinType)
         }
         }

+ 0 - 10
Trio/Sources/APS/Extensions/OmniBLEPumpManagerExtensions.swift

@@ -1,10 +0,0 @@
-import Foundation
-import OmniBLE
-import OmniKit
-
-public extension OmniBLEPumpManager {
-    static let managerIdentifier = "Omnipod-Dash"
-    var managerIdentifier: String {
-        OmniBLEPumpManager.managerIdentifier
-    }
-}

+ 0 - 10
Trio/Sources/APS/Extensions/OmniPodManagerExtensions.swift

@@ -1,10 +0,0 @@
-import Foundation
-import OmniKit
-
-public extension OmnipodPumpManager {
-    static let managerIdentifier = "Omnipod"
-
-    var managerIdentifier: String {
-        OmnipodPumpManager.managerIdentifier
-    }
-}

+ 9 - 11
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -284,17 +284,15 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             return
             return
         }
         }
 
 
-        // TODO: Fix backfill logic https://github.com/nightscout/Trio/issues/737
-        /*
-         let backfillGlucose = newGlucose.filter { $0.dateString <= syncDate }
-         if backfillGlucose.isNotEmpty {
-             debug(.deviceManager, "Backfilling glucose...")
-             do {
-                 try await glucoseStorage.storeGlucose(backfillGlucose)
-             } catch {
-                 debug(.deviceManager, "Unable to backfill glucose: \(error)")
-             }
-         }*/
+        let backfillGlucose = newGlucose.filter { $0.dateString <= syncDate }
+        if backfillGlucose.isNotEmpty {
+            debug(.deviceManager, "Backfilling glucose...")
+            do {
+                try await glucoseStorage.backfillGlucose(backfillGlucose)
+            } catch {
+                debug(.deviceManager, "Unable to backfill glucose: \(error)")
+            }
+        }
 
 
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)

+ 65 - 10
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -10,6 +10,7 @@ import Swinject
 protocol GlucoseStorage {
 protocol GlucoseStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeGlucose(_ glucose: [BloodGlucose]) async throws
     func storeGlucose(_ glucose: [BloodGlucose]) async throws
+    func backfillGlucose(_ glucose: [BloodGlucose]) async throws
     func addManualGlucose(glucose: Int)
     func addManualGlucose(glucose: Int)
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
@@ -62,10 +63,53 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return formatter
         return formatter
     }
     }
 
 
+    /// Backfills glucose values and stores in CoreData
+    ///
+    /// CGM managers will sometimes backfill glucose readings. To handle these backfilled values
+    /// correctly, we need some logic to handle a few cases:
+    ///  - _Not_ adding back previously deleted glucose
+    ///  - Avoiding duplicate values for the same reading
+    ///  - Avoiding overlapping glucose readings when switching sources
+    ///  Of these corner cases, overlapping glucose readings when switching sources is both
+    ///  the most challenging and most rare since it would happen if wearing two devices and
+    ///  switching or moving from direct glucose handling to xdrip. It's not worth the complexity
+    ///  to deal with source switching perfectly, so instead we will backfill glucose if and only if
+    ///  it isn't within 3.5 minutes of an existing glucose reading, which is simple but not perfect.
+    ///  But since this is a corner case that really shouldn't happen often, it's good enough.
+    func backfillGlucose(_ glucose: [BloodGlucose]) async throws {
+        try await context.perform {
+            // remove already deleted glucose values
+            let withoutDeletedGlucose = self.filterGlucoseValues(
+                glucose,
+                fetchRequest: DeletedGlucoseStored.fetchRequest(),
+                timeBuffer: 1
+            )
+
+            // check for a 3.5 minute difference between existing values
+            let filteredGlucose = self.filterGlucoseValues(
+                withoutDeletedGlucose,
+                fetchRequest: GlucoseStored.fetchRequest(),
+                timeBuffer: 3.5 * 60
+            )
+
+            guard !filteredGlucose.isEmpty else { return }
+
+            do {
+                // Store glucose values in Core Data
+                try self.storeGlucoseInCoreData(filteredGlucose)
+            } catch {
+                throw CoreDataError.creationError(
+                    function: #function,
+                    file: #fileID
+                )
+            }
+        }
+    }
+
     func storeGlucose(_ glucose: [BloodGlucose]) async throws {
     func storeGlucose(_ glucose: [BloodGlucose]) async throws {
         try await context.perform {
         try await context.perform {
             // Get new glucose values that don't exist yet
             // Get new glucose values that don't exist yet
-            let newGlucose = self.filterNewGlucoseValues(glucose)
+            let newGlucose = self.filterGlucoseValues(glucose, fetchRequest: GlucoseStored.fetchRequest(), timeBuffer: 1)
             guard !newGlucose.isEmpty else { return }
             guard !newGlucose.isEmpty else { return }
 
 
             do {
             do {
@@ -83,19 +127,22 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         }
         }
     }
     }
 
 
-    /// filter out duplicate CGM readings
+    /// filter out duplicate CGM readings using matching timestamps
     ///
     ///
-    /// This function will look through existing stored CGM values and filter out any new CGM values that
-    /// already exist. It does matching using dates and adds a small amount of time buffer for matching (1 second)
-    /// to account for precision loss that can happen with backfill CGM readings.
-    private func filterNewGlucoseValues(_ glucose: [BloodGlucose]) -> [BloodGlucose] {
+    /// This function will fetch dates from the `fetchRequest` and remove any glucose
+    /// values that are within `timeBuffer` of the fetched dates. This logic is useful for
+    /// deduplication checks or removing deleted CGM values from a list of backfilled readings.
+    private func filterGlucoseValues(
+        _ glucose: [BloodGlucose],
+        fetchRequest: NSFetchRequest<NSFetchRequestResult>,
+        timeBuffer: TimeInterval
+    ) -> [BloodGlucose] {
         let datesToCheck = glucose.map(\.dateString).sorted()
         let datesToCheck = glucose.map(\.dateString).sorted()
-        guard let firstDate = datesToCheck.first.map({ $0.addingTimeInterval(-1) }),
-              let lastDate = datesToCheck.last.map({ $0.addingTimeInterval(1) })
+        guard let firstDate = datesToCheck.first.map({ $0.addingTimeInterval(-timeBuffer) }),
+              let lastDate = datesToCheck.last.map({ $0.addingTimeInterval(timeBuffer) })
         else {
         else {
             return glucose
             return glucose
         }
         }
-        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
         fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
         fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
             NSPredicate(format: "date >= %@", firstDate as NSDate),
             NSPredicate(format: "date >= %@", firstDate as NSDate),
             NSPredicate(format: "date <= %@", lastDate as NSDate)
             NSPredicate(format: "date <= %@", lastDate as NSDate)
@@ -117,7 +164,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return glucose.filter { glucose in
         return glucose.filter { glucose in
             for existingDate in existingDates {
             for existingDate in existingDates {
                 let difference = abs(existingDate.timeIntervalSince(glucose.dateString))
                 let difference = abs(existingDate.timeIntervalSince(glucose.dateString))
-                if difference <= 1 {
+                if difference <= timeBuffer {
                     return false
                     return false
                 }
                 }
             }
             }
@@ -705,6 +752,14 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     return
                     return
                 }
                 }
 
 
+                // Create a new DeletedGlucoseStored object and copy the properties
+                if let date = glucoseToDelete.date {
+                    let deletedEntry = DeletedGlucoseStored(context: taskContext)
+                    deletedEntry.date = date
+                    deletedEntry.glucose = glucoseToDelete.glucose
+                    deletedEntry.isManualGlucoseEntry = glucoseToDelete.isManual
+                }
+
                 taskContext.delete(glucoseToDelete)
                 taskContext.delete(glucoseToDelete)
 
 
                 guard taskContext.hasChanges else { return }
                 guard taskContext.hasChanges else { return }

+ 45 - 34
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -545,41 +545,8 @@ final class BaseTDDStorage: TDDStorage, Injectable {
               let minutes = Int(timeComponents[1])
               let minutes = Int(timeComponents[1])
         else { return nil }
         else { return nil }
 
 
-        // Convert time to total minutes since midnight for easier comparison
         let totalMinutes = hours * 60 + minutes
         let totalMinutes = hours * 60 + minutes
-
-        // Special case: If profile has only one entry, it applies for full 24 hours
-        // Return its rate immediately without searching
-        if profile.count == 1 {
-            return profile[0].rate
-        }
-
-        // Use binary search to efficiently find the applicable basal rate
-        // Profile entries are sorted by minutes, so we can divide and conquer
-        var left = 0
-        var right = profile.count - 1
-
-        while left <= right {
-            let mid = (left + right) / 2
-            let entry = profile[mid]
-            // Get end time for current entry - either next entry's start time or end of day (1440 mins)
-            let nextMinutes = mid + 1 < profile.count ? profile[mid + 1].minutes : 1440
-
-            // Check if target time falls within current entry's time range
-            if totalMinutes >= entry.minutes, totalMinutes < nextMinutes {
-                return entry.rate
-            }
-
-            // Adjust search range based on comparison
-            if totalMinutes < entry.minutes {
-                right = mid - 1 // Search in left half if target time is earlier
-            } else {
-                left = mid + 1 // Search in right half if target time is later
-            }
-        }
-
-        // No applicable rate found for the given time
-        return nil
+        return findBasalRateForOffset(for: totalMinutes, in: profile)
     }
     }
 
 
     /// Calculates a weighted average of Total Daily Dose (TDD) based on recent and historical data
     /// Calculates a weighted average of Total Daily Dose (TDD) based on recent and historical data
@@ -692,3 +659,47 @@ extension Decimal {
         return result
         return result
     }
     }
 }
 }
+
+/// Finds the basal rate at the specified minute offset using binary search
+/// - Parameters:
+///   - totalMinutes: minute offset into a 24 hour day
+///   - profile: Array of basal profile entries sorted by time
+/// - Returns: Basal rate in units per hour, or nil if not found
+func findBasalRateForOffset(for totalMinutes: Int, in profile: [BasalProfileEntry]) -> Decimal? {
+    if profile.isEmpty {
+        return nil // not yet initalized
+    }
+
+    // Special case: If profile has only one entry, it applies for full 24 hours
+    // Return its rate immediately without searching
+    if profile.count == 1 {
+        return profile[0].rate
+    }
+
+    // Use binary search to efficiently find the applicable basal rate
+    // Profile entries are sorted by minutes, so we can divide and conquer
+    var left = 0
+    var right = profile.count - 1
+
+    while left <= right {
+        let mid = (left + right) / 2
+        let entry = profile[mid]
+        // Get end time for current entry - either next entry's start time or end of day (24 * 60 mins)
+        let nextMinutes = mid + 1 < profile.count ? profile[mid + 1].minutes : 24 * 60
+
+        // Check if target time falls within current entry's time range
+        if totalMinutes >= entry.minutes, totalMinutes < nextMinutes {
+            return entry.rate
+        }
+
+        // Adjust search range based on comparison
+        if totalMinutes < entry.minutes {
+            right = mid - 1 // Search in left half if target time is earlier
+        } else {
+            left = mid + 1 // Search in right half if target time is later
+        }
+    }
+
+    // No applicable rate found for the given time
+    return nil
+}

+ 3 - 0
Trio/Sources/Application/TrioApp.swift

@@ -407,6 +407,8 @@ extension Notification.Name {
 
 
     private func purgeOldNSManagedObjects() async throws {
     private func purgeOldNSManagedObjects() async throws {
         async let glucoseDeletion: () = coreDataStack.batchDeleteOlderThan(GlucoseStored.self, dateKey: "date", days: 90)
         async let glucoseDeletion: () = coreDataStack.batchDeleteOlderThan(GlucoseStored.self, dateKey: "date", days: 90)
+        async let archivedGlucoseDeletion: () = coreDataStack
+            .batchDeleteOlderThan(DeletedGlucoseStored.self, dateKey: "date", days: 90)
         async let pumpEventDeletion: () = coreDataStack.batchDeleteOlderThan(PumpEventStored.self, dateKey: "timestamp", days: 90)
         async let pumpEventDeletion: () = coreDataStack.batchDeleteOlderThan(PumpEventStored.self, dateKey: "timestamp", days: 90)
         async let bolusDeletion: () = coreDataStack.batchDeleteOlderThan(
         async let bolusDeletion: () = coreDataStack.batchDeleteOlderThan(
             parentType: PumpEventStored.self,
             parentType: PumpEventStored.self,
@@ -441,6 +443,7 @@ extension Notification.Name {
 
 
         // Await each task to ensure they are all completed
         // Await each task to ensure they are all completed
         try await glucoseDeletion
         try await glucoseDeletion
+        try await archivedGlucoseDeletion
         try await pumpEventDeletion
         try await pumpEventDeletion
         try await bolusDeletion
         try await bolusDeletion
         try await tempBasalDeletion
         try await tempBasalDeletion

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

@@ -37,6 +37,14 @@ extension Formatter {
         return formatter
         return formatter
     }()
     }()
 
 
+    static let decimalFormatterWithThreeFractionDigits: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.locale = .current
+        formatter.maximumFractionDigits = 3
+        return formatter
+    }()
+
     static let dateFormatter: DateFormatter = {
     static let dateFormatter: DateFormatter = {
         let dateFormatter = DateFormatter()
         let dateFormatter = DateFormatter()
         dateFormatter.timeStyle = .short
         dateFormatter.timeStyle = .short

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

@@ -12,6 +12,7 @@ struct OpenAPSStatus: JSON {
     let suggested: Determination?
     let suggested: Determination?
     let enacted: Determination?
     let enacted: Determination?
     let version: String
     let version: String
+    let recommendedBolus: Decimal?
 }
 }
 
 
 struct NSPumpStatus: JSON {
 struct NSPumpStatus: JSON {
@@ -19,6 +20,7 @@ struct NSPumpStatus: JSON {
     let battery: Battery?
     let battery: Battery?
     let reservoir: Decimal?
     let reservoir: Decimal?
     let status: PumpStatus?
     let status: PumpStatus?
+    let bolusIncrement: Decimal
 }
 }
 
 
 struct Uploader: JSON {
 struct Uploader: JSON {

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

@@ -253,7 +253,7 @@ extension Preferences: Decodable {
         }
         }
 
 
         if let bolusIncrement = try? container.decode(Decimal.self, forKey: .bolusIncrement) {
         if let bolusIncrement = try? container.decode(Decimal.self, forKey: .bolusIncrement) {
-            preferences.bolusIncrement = bolusIncrement
+            preferences.bolusIncrement = bolusIncrement > 0 ? bolusIncrement : 0.1
         }
         }
 
 
         if let curve = try? container.decode(InsulinCurve.self, forKey: .curve) {
         if let curve = try? container.decode(InsulinCurve.self, forKey: .curve) {

+ 6 - 6
Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -113,6 +113,12 @@ extension BasalProfileEditor {
                         // Successfully saved and synced
                         // Successfully saved and synced
                         self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
                         self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
 
 
+                        DispatchQueue.main.async {
+                            self.broadcaster.notify(BasalProfileObserver.self, on: .main) {
+                                $0.basalProfileDidChange(profile)
+                            }
+                        }
+
                         Task.detached(priority: .low) {
                         Task.detached(priority: .low) {
                             do {
                             do {
                                 debug(.nightscout, "Attempting to upload basal rates to Nightscout")
                                 debug(.nightscout, "Attempting to upload basal rates to Nightscout")
@@ -130,12 +136,6 @@ extension BasalProfileEditor {
                     print("We were successful")
                     print("We were successful")
                 }
                 }
                 .store(in: &lifetime)
                 .store(in: &lifetime)
-
-            DispatchQueue.main.async {
-                self.broadcaster.notify(BasalProfileObserver.self, on: .main) {
-                    $0.basalProfileDidChange(profile)
-                }
-            }
         }
         }
 
 
         @MainActor func validate() {
         @MainActor func validate() {

+ 3 - 3
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -621,7 +621,7 @@ extension DataTable {
                     Image(systemName: "circle.fill").foregroundColor(Color.insulin)
                     Image(systemName: "circle.fill").foregroundColor(Color.insulin)
                     Text(bolus.isSMB ? "SMB" : item.type ?? "Bolus")
                     Text(bolus.isSMB ? "SMB" : item.type ?? "Bolus")
                     Text(
                     Text(
-                        (Formatter.decimalFormatterWithTwoFractionDigits.string(from: amount) ?? "0") +
+                        (Formatter.decimalFormatterWithThreeFractionDigits.string(from: amount) ?? "0") +
                             String(localized: " U", comment: "Insulin unit")
                             String(localized: " U", comment: "Insulin unit")
                     )
                     )
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
@@ -632,7 +632,7 @@ extension DataTable {
                     Image(systemName: "circle.fill").foregroundColor(Color.insulin.opacity(0.4))
                     Image(systemName: "circle.fill").foregroundColor(Color.insulin.opacity(0.4))
                     Text("Temp Basal")
                     Text("Temp Basal")
                     Text(
                     Text(
-                        (Formatter.decimalFormatterWithTwoFractionDigits.string(from: rate) ?? "0") +
+                        (Formatter.decimalFormatterWithThreeFractionDigits.string(from: rate) ?? "0") +
                             String(localized: " U/hr", comment: "Unit insulin per hour")
                             String(localized: " U/hr", comment: "Unit insulin per hour")
                     )
                     )
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
@@ -657,7 +657,7 @@ extension DataTable {
                             alertTitle = String(localized: "Delete Insulin?", comment: "Alert title for deleting insulin")
                             alertTitle = String(localized: "Delete Insulin?", comment: "Alert title for deleting insulin")
                             alertMessage = Formatter.dateFormatter
                             alertMessage = Formatter.dateFormatter
                                 .string(from: item.timestamp ?? Date()) + ", " +
                                 .string(from: item.timestamp ?? Date()) + ", " +
-                                (Formatter.decimalFormatterWithTwoFractionDigits.string(from: item.bolus?.amount ?? 0) ?? "0") +
+                                (Formatter.decimalFormatterWithThreeFractionDigits.string(from: item.bolus?.amount ?? 0) ?? "0") +
                                 String(localized: " U", comment: "Insulin unit")
                                 String(localized: " U", comment: "Insulin unit")
 
 
                             if let bolus = item.bolus {
                             if let bolus = item.bolus {

+ 2 - 6
Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -41,15 +41,11 @@ extension Home.StateModel {
         insulinFromPersistence = insulinObjects
         insulinFromPersistence = insulinObjects
 
 
         manualTempBasal = apsManager.isManualTempBasal
         manualTempBasal = apsManager.isManualTempBasal
-        tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
+        tempBasals = insulinFromPersistence.filter { $0.tempBasal != nil }
 
 
-        suspensions = insulinFromPersistence.filter {
+        suspendAndResumeEvents = insulinFromPersistence.filter {
             $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
             $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
         }
         }
-        let lastSuspension = suspensions.last
-
-        pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
-            .type == EventType.pumpSuspend.rawValue
     }
     }
 
 
     // Setup Last Bolus to display the bolus progress bar
     // Setup Last Bolus to display the bolus progress bar

+ 14 - 4
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -2,6 +2,7 @@ import CGMBLEKitUI
 import Combine
 import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import LoopKit
 import LoopKitUI
 import LoopKitUI
 import Observation
 import Observation
 import SwiftDate
 import SwiftDate
@@ -40,7 +41,6 @@ extension Home {
         var targetProfiles: [TargetProfile] = []
         var targetProfiles: [TargetProfile] = []
         var timerDate = Date()
         var timerDate = Date()
         var closedLoop = false
         var closedLoop = false
-        var pumpSuspended = false
         var isLooping = false
         var isLooping = false
         var statusTitle = ""
         var statusTitle = ""
         var lastLoopDate: Date = .distantPast
         var lastLoopDate: Date = .distantPast
@@ -92,7 +92,7 @@ extension Home {
         var fetchedTDDs: [TDD] = []
         var fetchedTDDs: [TDD] = []
         var insulinFromPersistence: [PumpEventStored] = []
         var insulinFromPersistence: [PumpEventStored] = []
         var tempBasals: [PumpEventStored] = []
         var tempBasals: [PumpEventStored] = []
-        var suspensions: [PumpEventStored] = []
+        var suspendAndResumeEvents: [PumpEventStored] = []
         var batteryFromPersistence: [OpenAPS_Battery] = []
         var batteryFromPersistence: [OpenAPS_Battery] = []
         var lastPumpBolus: PumpEventStored?
         var lastPumpBolus: PumpEventStored?
         var overrides: [OverrideStored] = []
         var overrides: [OverrideStored] = []
@@ -107,6 +107,7 @@ extension Home {
         var cgmAvailable: Bool = false
         var cgmAvailable: Bool = false
         var listOfCGM: [CGMModel] = []
         var listOfCGM: [CGMModel] = []
         var cgmCurrent = cgmDefaultModel
         var cgmCurrent = cgmDefaultModel
+        var pumpInitialSettings = PumpConfig.PumpInitialSettings.default
         var shouldRunDeleteOnSettingsChange = true
         var shouldRunDeleteOnSettingsChange = true
 
 
         var showCarbsRequiredBadge: Bool = true
         var showCarbsRequiredBadge: Bool = true
@@ -550,9 +551,11 @@ extension Home {
         }
         }
 
 
         private func setupPumpSettings() async {
         private func setupPumpSettings() async {
-            let maxBasal = await provider.pumpSettings().maxBasal
+            let settings = await provider.pumpSettings()
             await MainActor.run {
             await MainActor.run {
-                self.maxBasal = maxBasal
+                self.maxBasal = settings.maxBasal
+                self.pumpInitialSettings.maxBasalRateUnitsPerHour = Double(settings.maxBasal)
+                self.pumpInitialSettings.maxBolusUnits = Double(settings.maxBolus)
             }
             }
         }
         }
 
 
@@ -560,6 +563,13 @@ extension Home {
             let basalProfile = await provider.getBasalProfile()
             let basalProfile = await provider.getBasalProfile()
             await MainActor.run {
             await MainActor.run {
                 self.basalProfile = basalProfile
                 self.basalProfile = basalProfile
+
+                if let schedule = BasalRateSchedule(
+                    dailyItems: basalProfile
+                        .map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) }
+                ) {
+                    self.pumpInitialSettings.basalSchedule = schedule
+                }
             }
             }
         }
         }
 
 

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

@@ -104,7 +104,7 @@ extension MainChartView {
     }
     }
 
 
     func drawSuspensions() -> some ChartContent {
     func drawSuspensions() -> some ChartContent {
-        let suspensions = state.suspensions
+        let suspensions = state.suspendAndResumeEvents
         return ForEach(suspensions) { suspension in
         return ForEach(suspensions) { suspension in
             let now = Date()
             let now = Date()
 
 
@@ -154,7 +154,8 @@ extension MainChartView {
             let duration = temp.tempBasal?.duration ?? 0
             let duration = temp.tempBasal?.duration ?? 0
             let timestamp = temp.timestamp ?? Date()
             let timestamp = temp.timestamp ?? Date()
             let end = timestamp + duration.minutes
             let end = timestamp + duration.minutes
-            let isInsulinSuspended = state.suspensions.contains { $0.timestamp ?? now >= timestamp && $0.timestamp ?? now <= end }
+            let isInsulinSuspended = state.suspendAndResumeEvents
+                .contains { $0.timestamp ?? now >= timestamp && $0.timestamp ?? now <= end }
 
 
             let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
             let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
 
 

+ 6 - 1
Trio/Sources/Modules/Home/View/Header/PumpView.swift

@@ -142,7 +142,12 @@ struct PumpView: View {
         }
         }
 
 
         if hours >= 1 {
         if hours >= 1 {
-            return "\(hours)" + String(localized: "h", comment: "abbreviation for hours")
+            var remainingHoursString = "\(hours)" + String(localized: "h", comment: "abbreviation for hours")
+            if hours < 12 {
+                remainingHoursString += " " + "\(minutes)" +
+                    String(localized: "m", comment: "abbreviation for minutes")
+            }
+            return remainingHoursString
         }
         }
 
 
         return "\(minutes)" + String(localized: "m", comment: "abbreviation for minutes")
         return "\(minutes)" + String(localized: "m", comment: "abbreviation for minutes")

+ 73 - 35
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -54,11 +54,17 @@ extension Home {
         )) var latestTempTarget: FetchedResults<TempTargetStored>
         )) var latestTempTarget: FetchedResults<TempTargetStored>
 
 
         var bolusProgressFormatter: NumberFormatter {
         var bolusProgressFormatter: NumberFormatter {
+            let fractionDigits: Int = switch state.settingsManager.preferences.bolusIncrement {
+            case 0.1: 1
+            case 0.025: 3
+            default: 2
+            }
+
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
             formatter.minimum = 0
             formatter.minimum = 0
-            formatter.maximumFractionDigits = state.settingsManager.preferences.bolusIncrement > 0.05 ? 1 : 2
-            formatter.minimumFractionDigits = state.settingsManager.preferences.bolusIncrement > 0.05 ? 1 : 2
+            formatter.maximumFractionDigits = fractionDigits
+            formatter.minimumFractionDigits = fractionDigits
             formatter.allowsFloats = true
             formatter.allowsFloats = true
             formatter.roundingIncrement = Double(state.settingsManager.preferences.bolusIncrement) as NSNumber
             formatter.roundingIncrement = Double(state.settingsManager.preferences.bolusIncrement) as NSNumber
             return formatter
             return formatter
@@ -162,22 +168,51 @@ extension Home {
             }
             }
         }
         }
 
 
-        var tempBasalString: String? {
-            guard let lastTempBasal = state.tempBasals.last?.tempBasal, let tempRate = lastTempBasal.rate else {
+        var basalString: String? {
+            var rate: NSNumber = 0
+            var manualBasalString = ""
+
+            guard let apsManager = state.apsManager else {
                 return nil
                 return nil
             }
             }
-            let rateString = Formatter.decimalFormatterWithTwoFractionDigits.string(from: tempRate as NSNumber) ?? "0"
-            var manualBasalString = ""
 
 
-            if let apsManager = state.apsManager, apsManager.isManualTempBasal {
-                manualBasalString = String(
-                    localized:
-                    " - Manual Basal ⚠️",
-                    comment: "Manual Temp basal"
-                )
+            if apsManager.isScheduledBasal == true {
+                guard let scheduledRate = scheduledBasalDeliveryRate(at: Date()) else {
+                    return nil
+                }
+                rate = scheduledRate
+            } else {
+                guard let lastTempBasal = state.tempBasals.last?.tempBasal, let tempRate = lastTempBasal.rate else {
+                    return nil
+                }
+                if apsManager.isManualTempBasal {
+                    manualBasalString = String(
+                        localized: " - Manual Basal ⚠️",
+                        comment: "Manual Temp basal"
+                    )
+                }
+                rate = tempRate
             }
             }
 
 
-            return rateString + String(localized: " U/hr", comment: "Unit per hour with space") + manualBasalString
+            let rateString = Formatter.decimalFormatterWithThreeFractionDigits.string(from: rate) ?? "0"
+            return rateString + String(localized: " U/hr", comment: "Unit per hour with space") +
+                manualBasalString
+        }
+
+        // Returns the scheduled basal rate for the current time based on the saved basal scheduled.
+        // Would be better if in the future BasalDeliveryStatus could be updated to include this info.
+        func scheduledBasalDeliveryRate(at when: Date) -> NSNumber? {
+            let calendar = Calendar(identifier: .gregorian)
+            // calendar.timeZone = timeZone /// should come from pumpManager in case it's different!
+
+            let hours = calendar.component(.hour, from: when)
+            let minutes = calendar.component(.minute, from: when)
+            let totalMinutes = hours * 60 + minutes
+
+            if let rate = findBasalRateForOffset(for: totalMinutes, in: state.basalProfile) {
+                return NSDecimalNumber(decimal: rate)
+            }
+            return nil
         }
         }
 
 
         var overrideString: String? {
         var overrideString: String? {
@@ -467,31 +502,34 @@ extension Home {
                         .font(.callout)
                         .font(.callout)
                 } else {
                 } else {
                     HStack {
                     HStack {
-                        if state.pumpSuspended {
-                            Text("Pump suspended")
-                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                                .foregroundColor(.loopGray)
-                        } else if let tempBasalString = tempBasalString {
+                        /// Only display the insulin delivery rate info if the pump is not
+                        /// suspended and is available (e.g., pod is paired & not faulted).
+                        let pumpAvailable = state.apsManager.isScheduledBasal != nil
+                        if !state.apsManager.isSuspended && pumpAvailable {
                             Image(systemName: "drop.circle")
                             Image(systemName: "drop.circle")
                                 .font(.callout)
                                 .font(.callout)
                                 .foregroundColor(.insulinTintColor)
                                 .foregroundColor(.insulinTintColor)
-                            if tempBasalString.count > 5 {
-                                Text(tempBasalString)
-                                    .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                                    .lineLimit(1)
-                                    .minimumScaleFactor(0.85)
-                                    .truncationMode(.tail)
-                                    .allowsTightening(true)
+                            if let basalString = self.basalString {
+                                /// Adjust opacity when displaying a scheduled basal rate
+                                let opacity = state.apsManager?.isScheduledBasal == true ? 0.6 : 1.0
+                                if basalString.count > 5 {
+                                    Text(basalString)
+                                        .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                        .lineLimit(1)
+                                        .minimumScaleFactor(0.85)
+                                        .truncationMode(.tail)
+                                        .allowsTightening(true)
+                                        .opacity(opacity)
+                                } else {
+                                    // Short strings can just display normally
+                                    Text(basalString)
+                                        .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                        .opacity(opacity)
+                                }
                             } else {
                             } else {
-                                // Short strings can just display normally
-                                Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                Text("No Data")
+                                    .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                             }
                             }
-                        } else {
-                            Image(systemName: "drop.circle")
-                                .font(.callout)
-                                .foregroundColor(.insulinTintColor)
-                            Text("No Data")
-                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                         }
                         }
                     }
                     }
                 }
                 }
@@ -736,7 +774,7 @@ extension Home {
                 let bolusString =
                 let bolusString =
                     (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
                     (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
                         + String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view") +
                         + String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view") +
-                        (Formatter.decimalFormatterWithTwoFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
+                        (Formatter.decimalFormatterWithThreeFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
                         + String(localized: " U", comment: "Insulin unit")
                         + String(localized: " U", comment: "Insulin unit")
 
 
                 ZStack {
                 ZStack {
@@ -964,7 +1002,7 @@ extension Home {
                 } else {
                 } else {
                     PumpConfig.PumpSetupView(
                     PumpConfig.PumpSetupView(
                         pumpType: state.setupPumpType,
                         pumpType: state.setupPumpType,
-                        pumpInitialSettings: PumpConfig.PumpInitialSettings.default,
+                        pumpInitialSettings: state.pumpInitialSettings,
                         bluetoothManager: state.provider.apsManager.bluetoothManager!,
                         bluetoothManager: state.provider.apsManager.bluetoothManager!,
                         completionDelegate: state,
                         completionDelegate: state,
                         setupDelegate: state
                         setupDelegate: state

+ 1 - 1
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -781,7 +781,7 @@ extension Onboarding {
                                 .bolusIncrement
                                 .bolusIncrement
                         )
                         )
                 )
                 )
-                preferences.bolusIncrement = bolusIncrement != 0.025 ? bolusIncrement : 0.1
+                preferences.bolusIncrement = bolusIncrement > 0 ? bolusIncrement : 0.1
             }
             }
 
 
             settingsManager.preferences = preferences
             settingsManager.preferences = preferences

+ 3 - 3
Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift

@@ -14,9 +14,9 @@ enum PumpConfig {
     }
     }
 
 
     struct PumpInitialSettings {
     struct PumpInitialSettings {
-        let maxBolusUnits: Double
-        let maxBasalRateUnitsPerHour: Double
-        let basalSchedule: BasalRateSchedule
+        var maxBolusUnits: Double
+        var maxBasalRateUnitsPerHour: Double
+        var basalSchedule: BasalRateSchedule
 
 
         static let `default` = PumpInitialSettings(
         static let `default` = PumpInitialSettings(
             maxBolusUnits: 10,
             maxBolusUnits: 10,

+ 10 - 5
Trio/Sources/Modules/Treatments/View/PopupView.swift

@@ -915,7 +915,7 @@ struct PopupView: View {
 
 
                     // Final insulin recommendation
                     // Final insulin recommendation
                     HStack(alignment: .firstTextBaseline, spacing: 4) {
                     HStack(alignment: .firstTextBaseline, spacing: 4) {
-                        Text(insulinFormatter(state.insulinCalculated))
+                        Text(insulinFormatter(state.insulinCalculated, .down, true))
                             .largeSolutionStyle()
                             .largeSolutionStyle()
                             .foregroundStyle(state.insulinCalculated > 0 ? Color.accentColor : .primary)
                             .foregroundStyle(state.insulinCalculated > 0 ? Color.accentColor : .primary)
 
 
@@ -939,19 +939,24 @@ struct PopupView: View {
     /// - Parameters:
     /// - Parameters:
     ///   - value: The insulin value to format
     ///   - value: The insulin value to format
     ///   - roundingMode: The rounding mode to apply (default: .down for conservative dosing)
     ///   - roundingMode: The rounding mode to apply (default: .down for conservative dosing)
+    ///   - roundedForPump: Use a max of 3 fraction digits when rounding for pump to accomidate for 0.025 U increments if applicable
     /// - Returns: A formatted string with 2 decimal places
     /// - Returns: A formatted string with 2 decimal places
-    private func insulinFormatter(_ value: Decimal, _ roundingMode: NSDecimalNumber.RoundingMode = .down) -> String {
+    private func insulinFormatter(
+        _ value: Decimal,
+        _ roundingMode: NSDecimalNumber.RoundingMode = .down,
+        _ roundedForPump: Bool = false
+    ) -> String {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.numberStyle = .decimal
         formatter.minimumFractionDigits = 2
         formatter.minimumFractionDigits = 2
-        formatter.maximumFractionDigits = 2
+        formatter.maximumFractionDigits = roundedForPump ? 3 : 2
         formatter.locale = Locale.current
         formatter.locale = Locale.current
 
 
         // Create a decimal handler with the specified rounding behavior.
         // Create a decimal handler with the specified rounding behavior.
-        // Always rounds to 2 decimal places (0.01 U precision).
+        // Rounds to 2 decimal places (0.01 U precision), except when rounding for pump
         let handler = NSDecimalNumberHandler(
         let handler = NSDecimalNumberHandler(
             roundingMode: roundingMode,
             roundingMode: roundingMode,
-            scale: 2,
+            scale: roundedForPump ? 3 : 2,
             raiseOnExactness: false,
             raiseOnExactness: false,
             raiseOnOverflow: false,
             raiseOnOverflow: false,
             raiseOnUnderflow: false,
             raiseOnUnderflow: false,

+ 1 - 1
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -37,7 +37,7 @@ extension Treatments {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
             formatter.maximumIntegerDigits = 2
             formatter.maximumIntegerDigits = 2
-            formatter.maximumFractionDigits = 2
+            formatter.maximumFractionDigits = 3
             return formatter
             return formatter
         }
         }
 
 

+ 90 - 0
Trio/Sources/Services/Network/Nightscout/BaseNightscoutManager+Subscribers.swift

@@ -0,0 +1,90 @@
+import Combine
+import CoreData
+import Foundation
+
+extension BaseNightscoutManager {
+    /// Call once from init. Hooks up:
+    /// 1) external upload requests (NotificationCenter)
+    /// 2) Core Data change triggers → requests per upload pipeline
+    /// 3) Glucose storage updates → request glucose pipeline
+    func wireSubscribers() {
+        wireExternalUploadRequests()
+        wireCoreDataSubscribers()
+        wireGlucoseStorageSubscriber()
+    }
+
+    /// Listens for `.nightscoutUploadRequested`, converts userInfo pipelines to enums,
+    /// and requests those upload pipelines. Posts `.nightscoutUploadDidFinish` after enqueuing.
+    func wireExternalUploadRequests() {
+        Foundation.NotificationCenter.default.publisher(for: .nightscoutUploadRequested)
+            .sink { [weak self] note in
+                guard let self else { return }
+                let pipelines = (note.userInfo?[NightscoutNotificationKey.uploadPipelines] as? [String])?
+                    .compactMap(NightscoutUploadPipeline.init(rawValue:)) ?? []
+
+                for pipeline in pipelines { self.requestUpload(pipeline) }
+
+                var info: [AnyHashable: Any] = [NightscoutNotificationKey.uploadPipelines: pipelines.map(\.rawValue)]
+                if let src = note.userInfo?[NightscoutNotificationKey.source] { info[NightscoutNotificationKey.source] = src }
+                Foundation.NotificationCenter.default.post(name: .nightscoutUploadDidFinish, object: nil, userInfo: info)
+            }
+            .store(in: &subscriptions)
+    }
+
+    /// Maps Core Data entity changes into upload pipeline requests. We rely on
+    /// per-pipeline throttle so rapid changes don’t spam Nightscout.
+    func wireCoreDataSubscribers() {
+        coreDataPublisher?
+            .filteredByEntityName("OrefDetermination")
+            .sink { [weak self] _ in self?.requestUpload(.deviceStatus) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("OverrideStored")
+            .sink { [weak self] _ in self?.requestUpload(.overrides) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("OverrideRunStored")
+            .sink { [weak self] _ in self?.requestUpload(.overrides) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("TempTargetStored")
+            .sink { [weak self] _ in self?.requestUpload(.tempTargets) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("TempTargetRunStored")
+            .sink { [weak self] _ in self?.requestUpload(.tempTargets) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("PumpEventStored")
+            .sink { [weak self] _ in self?.requestUpload(.pumpHistory) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("CarbEntryStored")
+            .sink { [weak self] _ in self?.requestUpload(.carbs) }
+            .store(in: &subscriptions)
+
+        coreDataPublisher?
+            .filteredByEntityName("GlucoseStored")
+            .sink { [weak self] _ in
+                self?.requestUpload(.glucose)
+                self?.requestUpload(.manualGlucose)
+            }
+            .store(in: &subscriptions)
+    }
+
+    /// Glucose storage updates → request glucose pipeline
+    func wireGlucoseStorageSubscriber() {
+        glucoseStorage.updatePublisher
+            .receive(on: queue)
+            .sink { [weak self] _ in
+                self?.requestUpload(.glucose)
+            }
+            .store(in: &subscriptions)
+    }
+}

+ 96 - 166
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -28,7 +28,7 @@ protocol NightscoutManager: GlucoseSource {
 final class BaseNightscoutManager: NightscoutManager, Injectable {
 final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var keychain: Keychain!
     @Injected() private var keychain: Keychain!
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
-    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var overridesStorage: OverrideStorage!
     @Injected() private var overridesStorage: OverrideStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
@@ -38,17 +38,69 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() var healthkitManager: HealthKitManager!
     @Injected() var healthkitManager: HealthKitManager!
+    @Injected() private var bolusCalculationManager: BolusCalculationManager!
+    @Injected() private var apsManager: APSManager!
 
 
-    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
-    private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
-    private let uploadPumpHistorySubject = PassthroughSubject<Void, Never>()
-    private let uploadCarbsSubject = PassthroughSubject<Void, Never>()
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var ping: TimeInterval?
     private var ping: TimeInterval?
 
 
-    private var backgroundContext = CoreDataStack.shared.newTaskContext()
+    // Queue where upload pipelines run.
+    let uploadPipelineQueue = DispatchQueue(label: "NightscoutManager.uploadPipelines", qos: .utility)
+
+    // Background Core Data context for fetches used by upload tasks.
+    var backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    /// Throttle window (seconds) per upload pipeline. Any requests inside this window
+    /// coalesce into a single upload run for that pipeline.
+    let uploadPipelineInterval: [NightscoutUploadPipeline: TimeInterval] = [
+        .carbs: 2, .pumpHistory: 2, .overrides: 2, .tempTargets: 2,
+        .glucose: 2, .manualGlucose: 2, .deviceStatus: 2
+    ]
+
+    /// Subjects used to request an upload pipeline. The pipeline applies a throttle so
+    /// close calls don’t double-upload.
+    var uploadPipelineSubjects: [NightscoutUploadPipeline: PassthroughSubject<Void, Never>] = {
+        var d: [NightscoutUploadPipeline: PassthroughSubject<Void, Never>] = [:]
+        NightscoutUploadPipeline.allCases.forEach { d[$0] = PassthroughSubject<Void, Never>() }
+        return d
+    }()
+
+    /// Request an upload for a pipeline (enqueue work). Safe to call from anywhere.
+    func requestUpload(_ uploadPipeline: NightscoutUploadPipeline) {
+        uploadPipelineSubjects[uploadPipeline]?.send(())
+    }
+
+    /// Build the Combine pipelines for all upload pipelines: subject → throttle → upload.
+    /// Must be called once during init().
+    func setupLanePipelines() {
+        for pipeline in NightscoutUploadPipeline.allCases {
+            guard let subject = uploadPipelineSubjects[pipeline], let window = uploadPipelineInterval[pipeline] else { continue }
+            subject
+                .receive(on: uploadPipelineQueue)
+                .throttle(for: .seconds(window), scheduler: uploadPipelineQueue, latest: false)
+                .sink { [weak self] in
+                    guard let self else { return }
+                    Task(priority: .utility) { await self.runUploadPipeline(pipeline) }
+                }
+                .store(in: &subscriptions)
+        }
+    }
 
 
-    private var lifetime = Lifetime()
+    /// Runs the actual upload for a single upload pipeline.
+    /// Called by the throttled pipeline, not directly by callers.
+    func runUploadPipeline(_ uploadPipeline: NightscoutUploadPipeline) async {
+        switch uploadPipeline {
+        case .carbs: await uploadCarbs()
+        case .pumpHistory: await uploadPumpHistory()
+        case .overrides: await uploadOverrides()
+        case .tempTargets: await uploadTempTargets()
+        case .glucose: await uploadGlucose()
+        case .manualGlucose: await uploadManualGlucose()
+        case .deviceStatus:
+            do { try await uploadDeviceStatus() }
+            catch { debug(.nightscout, "deviceStatus upload failed: \(error)") }
+        }
+    }
 
 
     private var isNetworkReachable: Bool {
     private var isNetworkReachable: Bool {
         reachabilityManager.isReachable
         reachabilityManager.isReachable
@@ -80,11 +132,14 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private var lastSuggestedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
 
 
     // Queue for handling Core Data change notifications
     // Queue for handling Core Data change notifications
-    private let queue = DispatchQueue(label: "BaseNightscoutManager.queue", qos: .background)
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
-    private var subscriptions = Set<AnyCancellable>()
+    let queue = DispatchQueue(label: "BaseNightscoutManager.queue", qos: .utility)
+
+    /// Emits changed Core Data object IDs from the app. We filter by entity names
+    /// and request upload pipelines based on what changed.
+    var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
 
 
-    private let debouncedQueue = DispatchQueue(label: "OrefDeterminationDebounce", qos: .utility)
+    /// Bag for Combine subscriptions owned by this manager.
+    var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
@@ -96,10 +151,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 
-        registerSubscribers()
-        registerHandlers()
         setupNotification()
         setupNotification()
 
 
+        setupLanePipelines()
+        wireSubscribers()
+
         /// Ensure that Nightscout Manager holds the `lastEnactedDetermination`, if one exists, on initialization.
         /// Ensure that Nightscout Manager holds the `lastEnactedDetermination`, if one exists, on initialization.
         /// We have to set this here in `init()`, so there's a `lastEnactedDetermination` available after an app restart
         /// We have to set this here in `init()`, so there's a `lastEnactedDetermination` available after an app restart
         /// for `uploadDeviceStatus()`, as within that fuction `lastEnactedDetermination` is reassigned at the very end of the function.
         /// for `uploadDeviceStatus()`, as within that fuction `lastEnactedDetermination` is reassigned at the very end of the function.
@@ -127,157 +183,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
     }
     }
 
 
-    private func registerHandlers() {
-        /// We add debouncing behavior here for two main reasons
-        /// 1. To ensure that any upload flag updates have properly been performed, and in subsequent fetching processes only truly unuploaded data is fetched
-        /// 2. To not spam the user's NS site with a high number of uploads in a very short amount of time (less than 1sec)
-        coreDataPublisher?
-            .filteredByEntityName("OrefDetermination")
-            .debounce(for: .seconds(2), scheduler: debouncedQueue)
-            .sink { [weak self] objectIDs in
-                guard let self = self else { return }
-
-                // Now hop onto the background context's queue
-                self.backgroundContext.perform {
-                    do {
-                        // Fetch only those determination objects
-                        let request: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
-                        request.predicate = NSPredicate(
-                            format: "SELF IN %@ AND isUploadedToNS == NO",
-                            objectIDs
-                        )
-                        let results = try self.backgroundContext.fetch(request)
-
-                        // If valid, proceed to send to subject for further processing
-                        if !results.isEmpty {
-                            Task {
-                                do {
-                                    try await self.uploadDeviceStatus()
-                                } catch {
-                                    debug(.nightscout, "\(DebuggingIdentifiers.failed) failed to upload device status")
-                                }
-                            }
-                        }
-                    } catch {
-                        debug(.nightscout, "\(DebuggingIdentifiers.failed) Failed to fetch OrefDetermination objects: \(error)")
-                    }
-                }
-            }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("OverrideStored")
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task.detached {
-                    await self.uploadOverrides()
-                }
-            }.store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("OverrideRunStored")
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task.detached {
-                    await self.uploadOverrides()
-                }
-            }.store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("TempTargetStored")
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task.detached {
-                    await self.uploadTempTargets()
-                }
-            }.store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("TempTargetRunStored")
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task.detached {
-                    await self.uploadTempTargets()
-                }
-            }.store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("PumpEventStored")
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] objectIDs in
-                guard let self = self else { return }
-
-                self.backgroundContext.perform {
-                    do {
-                        let request: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
-                        request.predicate = NSPredicate(
-                            format: "SELF IN %@ AND isUploadedToNS == NO",
-                            objectIDs
-                        )
-                        let results = try self.backgroundContext.fetch(request)
-
-                        if !results.isEmpty {
-                            Task.detached {
-                                await self.uploadPumpHistory()
-                            }
-                        }
-                    } catch {
-                        debugPrint("Failed to fetch PumpEventStored objects: \(error)")
-                    }
-                }
-            }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("CarbEntryStored")
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] objectIDs in
-                guard let self = self else { return }
-
-                // Now hop onto the background context’s queue
-                self.backgroundContext.perform {
-                    do {
-                        let request: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
-                        request.predicate = NSPredicate(
-                            format: "SELF IN %@ AND isUploadedToNS == NO",
-                            objectIDs
-                        )
-                        let results = try self.backgroundContext.fetch(request)
-
-                        // If valid, proceed to send to subject for further processing
-                        if !results.isEmpty {
-                            Task.detached {
-                                await self.uploadCarbs()
-                            }
-                        }
-                    } catch {
-                        debugPrint("Failed to fetch CarbEntryStored objects: \(error)")
-                    }
-                }
-            }
-            .store(in: &subscriptions)
-
-        coreDataPublisher?.filteredByEntityName("GlucoseStored")
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task.detached {
-                    await self.uploadGlucose()
-                    await self.uploadManualGlucose()
-                }
-            }
-            .store(in: &subscriptions)
-    }
-
-    func registerSubscribers() {
-        glucoseStorage.updatePublisher
-            .receive(on: queue)
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadGlucose()
-                }
-            }
-            .store(in: &subscriptions)
-    }
-
     func setupNotification() {
     func setupNotification() {
         Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
         Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
             .sink { [weak self] _ in
             .sink { [weak self] _ in
@@ -588,6 +493,29 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             fetchedEnactedDetermination = enacted
             fetchedEnactedDetermination = enacted
         }
         }
 
 
+        // Calculate recommended bolus
+        var recommendedBolus: Decimal = 0
+
+        if let latest = fetchedSuggestedDetermination ?? fetchedEnactedDetermination {
+            let minPredBG = latest.minPredBGFromReason ?? 0
+            let simulatedCOB: Int16? = latest.cob.map { Int16(truncating: NSDecimalNumber(decimal: $0)) }
+
+            let result = await bolusCalculationManager.handleBolusCalculation(
+                carbs: 0,
+                useFattyMealCorrection: false,
+                useSuperBolus: false,
+                lastLoopDate: apsManager.lastLoopDate,
+                minPredBG: minPredBG,
+                simulatedCOB: simulatedCOB,
+                isBackdated: false
+            )
+
+            recommendedBolus = apsManager.roundBolus(amount: result.insulinCalculated)
+        }
+
+        // Bolus increment
+        let bolusIncrement = settingsManager.preferences.bolusIncrement
+
         // Gather all relevant data for OpenAPS Status
         // Gather all relevant data for OpenAPS Status
         let iob = await fetchedIOBEntry
         let iob = await fetchedIOBEntry
 
 
@@ -598,7 +526,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             iob: iob?.first,
             iob: iob?.first,
             suggested: suggestedToUpload,
             suggested: suggestedToUpload,
             enacted: settingsManager.settings.closedLoop ? enactedToUpload : nil,
             enacted: settingsManager.settings.closedLoop ? enactedToUpload : nil,
-            version: Bundle.main.releaseVersionNumber ?? "Unknown"
+            version: Bundle.main.releaseVersionNumber ?? "Unknown",
+            recommendedBolus: recommendedBolus
         )
         )
 
 
         debug(.nightscout, "To be uploaded openapsStatus: \(openapsStatus)")
         debug(.nightscout, "To be uploaded openapsStatus: \(openapsStatus)")
@@ -611,7 +540,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             clock: Date(),
             clock: Date(),
             battery: battery,
             battery: battery,
             reservoir: reservoir != 0xDEAD_BEEF ? reservoir : nil,
             reservoir: reservoir != 0xDEAD_BEEF ? reservoir : nil,
-            status: pumpStatus
+            status: pumpStatus,
+            bolusIncrement: bolusIncrement
         )
         )
 
 
         let batteryLevel = await UIDevice.current.batteryLevel
         let batteryLevel = await UIDevice.current.batteryLevel

+ 42 - 0
Trio/Sources/Services/Network/Nightscout/NightscoutUploadPipeline.swift

@@ -0,0 +1,42 @@
+import Foundation
+
+/// Logical upload “paths” handled by NightscoutManager.
+/// Each upload pipeline has its own throttled queue so we don’t double-upload
+/// when multiple sources trigger the same work close together.
+public enum NightscoutUploadPipeline: String, CaseIterable {
+    case carbs
+    case pumpHistory
+    case overrides
+    case tempTargets
+    case glucose
+    case manualGlucose
+    case deviceStatus
+}
+
+/// Keys used in Nightscout upload notifications.
+public enum NightscoutNotificationKey {
+    /// Array of upload pipeline rawValues to upload, e.g. ["carbs", "pumpHistory"].
+    public static let uploadPipelines = "uploadPipelines"
+    /// Optional string that says who asked for the upload (debug/diagnostics).
+    public static let source = "source"
+}
+
+public extension Foundation.Notification.Name {
+    /// Post this to request one or more uploads by upload pipeline.
+    static let nightscoutUploadRequested = Notification.Name("nightscoutUploadRequested")
+    /// Posted after we enqueue all requested upload pipelines (not a network completion).
+    static let nightscoutUploadDidFinish = Notification.Name("nightscoutUploadDidFinish")
+}
+
+/// Convenience helper any component (e.g. APSManager) can call to
+/// request uploads. The work is enqueued and deduped per upload pipeline via throttle,
+/// so rapid duplicate calls won’t double-upload.
+///
+/// - Parameters:
+///   - uploadPipelines: Which pipelines to request (carbs, pumpHistory, etc).
+///   - source: Optional tag for debugging (e.g. "APSManager").
+public func requestNightscoutUpload(_ uploadPipelines: [NightscoutUploadPipeline], source: String? = nil) {
+    var userInfo: [AnyHashable: Any] = [NightscoutNotificationKey.uploadPipelines: uploadPipelines.map(\.rawValue)]
+    if let source { userInfo[NightscoutNotificationKey.source] = source }
+    Foundation.NotificationCenter.default.post(name: .nightscoutUploadRequested, object: nil, userInfo: userInfo)
+}

+ 11 - 0
TrioTests/CoreDataTests/GlucoseStorageTests.swift

@@ -99,6 +99,17 @@ import Testing
         ) as? [GlucoseStored]
         ) as? [GlucoseStored]
 
 
         #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
         #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
+
+        // Finally verify that it stored a copy
+        let archivedEntries = try await coreDataStack.fetchEntitiesAsync(
+            ofType: DeletedGlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 140"),
+            key: "date",
+            ascending: false
+        ) as? [DeletedGlucoseStored]
+
+        #expect(archivedEntries?.isEmpty == false, "Should have archived entries after deletion")
     }
     }
 
 
     @Test("Get glucose not yet uploaded to Nightscout") func testGetGlucoseNotYetUploadedToNightscout() async throws {
     @Test("Get glucose not yet uploaded to Nightscout") func testGetGlucoseNotYetUploadedToNightscout() async throws {

+ 1 - 1
dexcom-share-client-swift

@@ -1 +1 @@
-Subproject commit 41cf95dab00f125f7a7602c433aac79fea8fc549
+Subproject commit 82a9179d444b3e79d5e9cfe99bbe7f298c4e8b40

+ 2 - 2
fastlane/testflight.md

@@ -180,8 +180,8 @@ _Referring to the table below, tap on each **IDENTIFIER** that has a different *
 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the Apple developer site.
 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the Apple developer site.
 1. Repeat this step for these three Identifier **NAMES** - refer to the [Table](#table-of-identifiers) above if your Names look different; if they do, see [Optional: Identifier Description Modification](#optional-identifier-description-modification)
 1. Repeat this step for these three Identifier **NAMES** - refer to the [Table](#table-of-identifiers) above if your Names look different; if they do, see [Optional: Identifier Description Modification](#optional-identifier-description-modification)
     * Trio
     * Trio
-    * Trio Watch
-    * Trio WatchKit Extension
+    * Trio Watch App
+    * Trio Watch Complication
 1. Click on the **IDENTIFIER** row.
 1. Click on the **IDENTIFIER** row.
 1. Scroll down to the "App Groups" capabilies row, click on the "Configure" (or "Edit") button.
 1. Scroll down to the "App Groups" capabilies row, click on the "Configure" (or "Edit") button.
 1. Select the "Trio App Group" _(yes, "Trio App Group" is correct)_
 1. Select the "Trio App Group" _(yes, "Trio App Group" is correct)_

+ 3 - 1
oref0_source_version.txt

@@ -1,6 +1,8 @@
-oref0 branch: remove-400-guard-trio-oref - git version: 3aeba68
+oref0 branch: dev - git version: 8282ce7
 
 
 Last commits:
 Last commits:
+8282ce7 Merge pull request #49 from nightscout/remove-400-guard-trio-oref
+2319a16 feat(algo): Remove short and long delta condition to avoid early exit breaking loop; set 30min neutral temp like original openaps/oref0 does
 3aeba68 feat(algo): Remove 400 / shouldProtectDueToHIGH guard #hackdiabetes2025
 3aeba68 feat(algo): Remove 400 / shouldProtectDueToHIGH guard #hackdiabetes2025
 37896e5 Merge pull request #48 from nightscout/fix-bundle-naming
 37896e5 Merge pull request #48 from nightscout/fix-bundle-naming
 f21a187 Rename output library to trio_[name]
 f21a187 Rename output library to trio_[name]