Selaa lähdekoodia

Merge branch 'core-data-sync-trio' of github.com:nightscout/Trio-dev into watch

Deniz Cengiz 1 vuosi sitten
vanhempi
commit
ed257d7ce3
55 muutettua tiedostoa jossa 1001 lisäystä ja 343 poistoa
  1. 1 1
      Model/Classes+Properties/OpenAPS_Battery+CoreDataProperties.swift
  2. 2 2
      Model/Helper/CarbEntryStored+helper.swift
  3. 5 0
      Model/Helper/NSPredicates.swift
  4. 1 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  5. 8 0
      Trio.xcodeproj/project.pbxproj
  6. 1 1
      Trio/Resources/javascript/bundle/autosens.js
  7. 1 1
      Trio/Resources/javascript/bundle/autotune-prep.js
  8. 1 1
      Trio/Resources/javascript/bundle/determine-basal.js
  9. 1 1
      Trio/Resources/javascript/bundle/iob.js
  10. 1 1
      Trio/Resources/javascript/bundle/meal.js
  11. 1 1
      Trio/Resources/javascript/bundle/profile.js
  12. 13 11
      Trio/Sources/APS/APSManager.swift
  13. 1 1
      Trio/Sources/APS/DeviceDataManager.swift
  14. 2 1
      Trio/Sources/APS/FetchGlucoseManager.swift
  15. 38 41
      Trio/Sources/APS/Storage/CarbsStorage.swift
  16. 2 1
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  17. 2 0
      Trio/Sources/Application/TrioApp.swift
  18. 1 1
      Trio/Sources/Models/DecimalPickerSettings.swift
  19. 22 0
      Trio/Sources/Models/GlucoseNotificationsOption.swift
  20. 6 3
      Trio/Sources/Models/TrioSettings.swift
  21. 2 3
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  22. 1 1
      Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  23. 1 1
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  24. 1 1
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  25. 1 1
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  26. 11 9
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  27. 1 1
      Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  28. 3 1
      Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  29. 337 33
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  30. 229 0
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  31. 29 6
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  32. 2 2
      Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  33. 111 64
      Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  34. 0 3
      Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift
  35. 1 0
      Trio/Sources/Modules/Home/HomeStateModel.swift
  36. 1 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/CarbView.swift
  37. 2 1
      Trio/Sources/Modules/Home/View/Chart/MainChartView.swift
  38. 1 1
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  39. 8 1
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  40. 36 39
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  41. 6 1
      Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift
  42. 1 1
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  43. 2 2
      Trio/Sources/Modules/Settings/SettingItems.swift
  44. 2 7
      Trio/Sources/Modules/Stat/View/StatsView.swift
  45. 22 22
      Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  46. 2 1
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  47. 1 1
      Trio/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift
  48. 19 17
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  49. 14 16
      Trio/Sources/Modules/WatchConfig/View/WatchConfigGarminView.swift
  50. 5 1
      Trio/Sources/Router/Router.swift
  51. 19 18
      Trio/Sources/Services/HealthKit/HealthKitManager.swift
  52. 1 1
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  53. 7 11
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  54. 11 4
      oref0_source_version.txt
  55. 1 2
      trio-oref/lib/determine-basal/determine-basal.js

+ 1 - 1
Model/Classes+Properties/OpenAPS_Battery+CoreDataProperties.swift

@@ -9,7 +9,7 @@ public extension OpenAPS_Battery {
     @NSManaged var date: Date?
     @NSManaged var display: Bool
     @NSManaged var id: UUID?
-    @NSManaged var percent: Int16
+    @NSManaged var percent: Double
     @NSManaged var status: String?
     @NSManaged var voltage: NSDecimalNumber?
 }

+ 2 - 2
Model/Helper/CarbEntryStored+helper.swift

@@ -9,13 +9,13 @@ extension NSPredicate {
 
     static var carbsForChart: NSPredicate {
         let date = Date.oneDayAgo
-        return NSPredicate(format: "isFPU == false AND date >= %@", date as NSDate)
+        return NSPredicate(format: "isFPU == false AND date >= %@ AND carbs > 0", date as NSDate)
     }
 
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
-            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@",
+            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@ AND carbs > 0",
             date as NSDate,
             false as NSNumber,
             false as NSNumber

+ 5 - 0
Model/Helper/NSPredicates.swift

@@ -66,6 +66,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
 
+    static var carbsHistory: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND carbs > 0", date as NSDate)
+    }
+
     static var predicateForOneHourAgo: NSPredicate {
         let date = Date.oneHourAgo
         return NSPredicate(format: "date >= %@", date as NSDate)

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

@@ -96,7 +96,7 @@
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="display" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
-        <attribute name="percent" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="percent" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="status" optional="YES" attributeType="String"/>
         <attribute name="voltage" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <fetchIndex name="byDate">

+ 8 - 0
Trio.xcodeproj/project.pbxproj

@@ -270,6 +270,7 @@
 		6EADD581738D64431902AC0A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
+		715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */; };
 		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
@@ -324,6 +325,7 @@
 		BDA25F222D26D62800035F34 /* BolusInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25F212D26D62200035F34 /* BolusInputView.swift */; };
 		BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */; };
 		BDAE40002D372BAD009C12B1 /* WatchState+Requests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */; };
+		BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
@@ -990,6 +992,7 @@
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
+		715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationsOption.swift; sourceTree = "<group>"; };
 		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -1042,6 +1045,7 @@
 		BDA25F212D26D62200035F34 /* BolusInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusInputView.swift; sourceTree = "<group>"; };
 		BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetSetup.swift; sourceTree = "<group>"; };
 		BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchState+Requests.swift"; sourceTree = "<group>"; };
+		BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryEditorView.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
@@ -1402,6 +1406,7 @@
 		0EE66DD474AFFD4FD787D5B9 /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
 				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
 			);
 			path = View;
@@ -2050,6 +2055,7 @@
 				BD54A9722D281A9C00F9C1EE /* TempTargetPresetWatch.swift */,
 				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
 				BDA25EFC2D261BF200035F34 /* WatchState.swift */,
+				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
 				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
@@ -3919,6 +3925,7 @@
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
+				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
@@ -3939,6 +3946,7 @@
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
+				BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
 				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
Trio/Resources/javascript/bundle/autosens.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
Trio/Resources/javascript/bundle/autotune-prep.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
Trio/Resources/javascript/bundle/determine-basal.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
Trio/Resources/javascript/bundle/iob.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
Trio/Resources/javascript/bundle/meal.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
Trio/Resources/javascript/bundle/profile.js


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

@@ -1173,16 +1173,18 @@ final class BaseAPSManager: APSManager, Injectable {
             let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
 
             let hbs = Durations(
-                day: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) : self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                week: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) : self
-                    .roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                month: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) : self
-                    .roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                total: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) : self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
+                day: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
+                week: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
+                month: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
+                total: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
             )
 
             var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
@@ -1392,7 +1394,7 @@ extension BaseAPSManager: PumpManagerStatusObserver {
                 }
 
                 batteryToStore.date = Date()
-                batteryToStore.percent = Int16(percent)
+                batteryToStore.percent = Double(percent)
                 batteryToStore.voltage = nil
                 batteryToStore.status = percent > 10 ? "normal" : "low"
                 batteryToStore.display = status.pumpBatteryChargeRemaining != nil

+ 1 - 1
Trio/Sources/APS/DeviceDataManager.swift

@@ -132,7 +132,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                             let saveBatteryToCoreData = OpenAPS_Battery(context: self.privateContext)
                             saveBatteryToCoreData.id = UUID()
                             saveBatteryToCoreData.date = Date()
-                            saveBatteryToCoreData.percent = Int16(batteryPercent)
+                            saveBatteryToCoreData.percent = Double(batteryPercent)
                             saveBatteryToCoreData.voltage = nil
                             saveBatteryToCoreData.status = batteryPercent >= 10 ? BatteryState.normal.rawValue : BatteryState
                                 .low.rawValue

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

@@ -213,7 +213,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                     unfiltered: Decimal(result.glucose),
                     filtered: Decimal(result.glucose),
                     noise: nil,
-                    glucose: Int(result.glucose)
+                    glucose: Int(result.glucose),
+                    type: "sgv"
                 )
             }
         }

+ 38 - 41
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -11,12 +11,11 @@ protocol CarbsObserver {
 protocol CarbsStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
-    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
+    func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
     func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
 }
@@ -46,8 +45,29 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
 
-        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+        // Check for FPU-only entries (fat/protein without carbs)
+        let fpuOnlyEntries = entriesToStore.filter { entry in
+            entry.carbs == 0 && (entry.fat ?? 0 > 0 || entry.protein ?? 0 > 0)
+        }
+
+        // Create additional Carb (non-FPU) entries with fat/protein amounts and carbs == 0
+        for entry in fpuOnlyEntries {
+            let additionalEntry = CarbsEntry(
+                id: entry.id,
+                createdAt: entry.createdAt,
+                actualDate: entry.actualDate,
+                carbs: Decimal(0),
+                fat: entry.fat,
+                protein: entry.protein,
+                note: entry.note,
+                enteredBy: entry.enteredBy,
+                isFPU: false, // it should be a Carb entry
+                fpuID: entry.fpuID
+            )
+            entriesToStore.append(additionalEntry)
+        }
 
+        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
 
@@ -195,7 +215,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
-        guard let entry = entries.last, entry.carbs != 0 else { return }
+        guard let entry = entries.last else { return }
 
         await coredataContext.perform {
             let newItem = CarbEntryStored(context: self.coredataContext)
@@ -265,22 +285,26 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
 
-    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+    func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async {
         let taskContext = CoreDataStack.shared.newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.transactionAuthor = "deleteCarbs"
 
-        var carbEntry: CarbEntryStored?
+        var carbEntryFromCoreData: CarbEntryStored?
 
         await taskContext.perform {
             do {
-                carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
-                guard let carbEntry = carbEntry else {
+                carbEntryFromCoreData = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                guard let carbEntry = carbEntryFromCoreData else {
                     debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
                     return
                 }
 
-                if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                // entry has fpuID
+                // case 1: carb equivalent entry
+                // case 2: "parent" entry, but containing fat and/or protein, and possibly carbs
+                // => use fpuID ID to delete all corresponding entries via batch delete
+                if let fpuID = carbEntry.fpuID {
                     // fetch request for all carb entries with the same id
                     let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
                     fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
@@ -295,14 +319,17 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
                     // Notifiy subscribers of the batch delete
                     self.updateSubject.send(())
-                } else {
+                }
+                // entry has no fpuID
+                // => it's a carb-only entry. use its ID to for deletion
+                else {
                     taskContext.delete(carbEntry)
 
                     guard taskContext.hasChanges else { return }
                     try taskContext.save()
 
                     debugPrint(
-                        "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
+                        "CarbsStorage: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
                     )
                 }
 
@@ -312,36 +339,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
-        processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
-
-            if fpuID != "" {
-                if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
-                    debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
-                } else {
-                    allValues.removeAll(where: { $0.fpuID == fpuID })
-                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                        $0.carbsDidUpdate(allValues)
-                    }
-                }
-            }
-
-            if fpuID == "" || complex {
-                if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
-                    debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
-                } else {
-                    allValues.removeAll(where: { $0.id == uniqueID })
-                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                        $0.carbsDidUpdate(allValues)
-                    }
-                }
-            }
-        }
-    }
-
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,

+ 2 - 1
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -299,7 +299,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     unfiltered: Decimal(result.glucose),
                     filtered: Decimal(result.glucose),
                     noise: nil,
-                    glucose: Int(result.glucose)
+                    glucose: Int(result.glucose),
+                    type: "sgv"
                 )
             }
         }

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

@@ -48,6 +48,8 @@ import Swinject
         _ = resolver.resolve(FetchTreatmentsManager.self)!
         _ = resolver.resolve(CalendarManager.self)!
         _ = resolver.resolve(UserNotificationsManager.self)!
+        _ = resolver.resolve(WatchManager.self)!
+        _ = resolver.resolve(ContactImageManager.self)!
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(WatchManager.self)!
         _ = resolver.resolve(ContactImageManager.self)!

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

@@ -136,7 +136,7 @@ struct DecimalPickerSettings {
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, type: PickerSetting.PickerSettingType.hour)
     var maxBolus = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
-    var maxBasal = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxBasal = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
 }
 
 struct PickerSetting {

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

@@ -0,0 +1,22 @@
+//
+//  GlucoseNotificationOption.swift
+//  FreeAPS
+//
+//  Created by Kimberlie Skandis on 1/18/25.
+//
+import Foundation
+
+public enum GlucoseNotificationsOption: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    public var id: String { rawValue }
+    case disabled
+    case alwaysEveryCGM
+    case onlyAlarmLimits
+
+    var displayName: String {
+        switch self {
+        case .disabled: return "Disabled"
+        case .alwaysEveryCGM: return "Always"
+        case .onlyAlarmLimits: return "Only Alarm Limits"
+        }
+    }
+}

+ 6 - 3
Trio/Sources/Models/TrioSettings.swift

@@ -34,7 +34,7 @@ struct TrioSettings: JSON, Equatable {
     var notificationsCgm: Bool = true
     var notificationsCarb: Bool = true
     var notificationsAlgorithm: Bool = true
-    var glucoseNotificationsAlways: Bool = false
+    var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
     var useAlarmSound: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
     var lowGlucose: Decimal = 72
@@ -200,8 +200,11 @@ extension TrioSettings: Decodable {
             settings.notificationsAlgorithm = notificationsAlgorithm
         }
 
-        if let glucoseNotificationsAlways = try? container.decode(Bool.self, forKey: .glucoseNotificationsAlways) {
-            settings.glucoseNotificationsAlways = glucoseNotificationsAlways
+        if let glucoseNotificationsOption = try? container.decode(
+            GlucoseNotificationsOption.self,
+            forKey: .glucoseNotificationsOption
+        ) {
+            settings.glucoseNotificationsOption = glucoseNotificationsOption
         }
 
         if let useAlarmSound = try? container.decode(Bool.self, forKey: .useAlarmSound) {

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

@@ -189,13 +189,12 @@ extension Adjustments.StateModel {
     func updateLatestOverrideConfiguration() {
         Task { [weak self] in
             guard let self = self else { return }
-            
+
             let id = await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
-            
+
             // execute sequentially instead of concurrently
             await self.updateLatestOverrideConfigurationOfState(from: id)
             await self.setCurrentOverride(from: id)
-            
         }
     }
 

+ 1 - 1
Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift

@@ -74,7 +74,7 @@ struct AddOverrideForm: View {
             Section(footer: state.percentageDescription(state.overridePercentage)) {
                 // Percentage Picker
                 HStack {
-                    Text("Change Basal Rate by")
+                    Text("Basal Rate Adjustment")
                     Spacer()
                     Text("\(state.overridePercentage.formatted(.number)) %")
                         .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)

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

@@ -94,7 +94,7 @@ extension Adjustments.RootView {
                 selectedOverride = preset
                 isConfirmDeletePresented = true
             } label: {
-                Label("Delete", systemImage: "trash")
+                Label("Delete", systemImage: "trash.fill")
                     .tint(.red)
             }
             Button(action: {

+ 1 - 1
Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift

@@ -140,7 +140,7 @@ struct EditOverrideForm: View {
             // Percentage Picker
             Section(footer: state.percentageDescription(percentage)) {
                 HStack {
-                    Text("Change Basal Rate by")
+                    Text("Basal Rate Adjustment")
                     Spacer()
                     Text("\(percentage.formatted(.number)) %")
                         .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)

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

@@ -82,7 +82,7 @@ extension Adjustments.RootView {
                     isConfirmDeletePresented = true
                 }
             } label: {
-                Label("Delete", systemImage: "trash")
+                Label("Delete", systemImage: "trash.fill")
                     .tint(.red)
             }
             Button(action: {

+ 11 - 9
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -84,23 +84,25 @@ extension BasalProfileEditor {
 
                 Group {
                     HStack {
-                        Button {
+                        Button(action: {
                             let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                             impactHeavy.impactOccurred()
                             state.save()
-                        } label: {
+                        }, label: {
                             HStack {
                                 if state.syncInProgress {
                                     ProgressView().padding(.trailing, 10)
                                 }
                                 Text(state.syncInProgress ? "Saving..." : "Save")
-                            }.padding(10)
-                        }
-                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
-                        .disabled(shouldDisableButton)
-                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
-                        .tint(.white)
-                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                            }
+                            .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                            .padding(10)
+                        })
+                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                            .disabled(shouldDisableButton)
+                            .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                            .tint(.white)
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
                     }
                 }.padding(5)
             }

+ 1 - 1
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -138,7 +138,7 @@ extension BolusCalculatorConfig {
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
-                        Text("Default Percent: 200%").bold()
+                        Text("Default Percent: 100%").bold()
                         Text("Do not enable this feature until you have optimized your CR (carb ratio) setting.").bold()
                         Text(
                             "Enabling this setting adds a \"Super Bolus\" option to the bolus calculator. Once this feature is enabled, a percentage setting will appear for you to select."

+ 3 - 1
Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -53,7 +53,9 @@ extension CarbRatioEditor {
                                         ProgressView().padding(.trailing, 10)
                                     }
                                     Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                                }.padding(10)
+                                }
+                                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                                .padding(10)
                             }
                         }
                         .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)

+ 337 - 33
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -18,8 +18,6 @@ extension DataTable {
 
         var mode: Mode = .treatments
         var treatments: [Treatment] = []
-        var glucose: [Glucose] = []
-        var meals: [Treatment] = []
         var manualGlucose: Decimal = 0
         var waitForSuggestion: Bool = false
 
@@ -28,18 +26,24 @@ extension DataTable {
 
         var units: GlucoseUnits = .mgdL
 
+        var carbEntryToEdit: CarbEntryStored?
+        var showCarbEntryEditor = false
+
         override func subscribe() {
             units = settingsManager.settings.units
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
         }
 
+        /// Checks if the glucose data is fresh based on the given date
+        /// - Parameter glucoseDate: The date to check
+        /// - Returns: Boolean indicating if the data is fresh
         func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
 
-        // Glucose deletion from history and from remote services
-        /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        /// Initiates the glucose deletion process asynchronously
+        /// - Parameter treatmentObjectID: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
                 await deleteGlucose(treatmentObjectID)
@@ -99,9 +103,9 @@ extension DataTable {
 
         // Carb and FPU deletion from history
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
             Task {
-                await deleteCarbs(treatmentObjectID)
+                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
 
                 await MainActor.run {
                     carbEntryDeleted = true
@@ -110,34 +114,58 @@ extension DataTable {
             }
         }
 
-        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
-            // Delete from Apple Health/Tidepool
-            await deleteCarbsFromServices(treatmentObjectID)
+        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async {
+            // Delete from Nightscout/Apple Health/Tidepool
+            await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
 
             // Delete carbs from Core Data
-            await carbsStorage.deleteCarbs(treatmentObjectID)
+            await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
 
             // Perform a determine basal sync to update cob
             await apsManager.determineBasalSync()
         }
 
-        func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
+        /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
+        /// - Parameters:
+        ///   - treatmentObjectID: The Core Data object ID of the entry to delete
+        ///   - isFPUDeletion: Flag indicating if this is a FPU deletion that requires special handling
+        ///     - If true: Will first fetch the corresponding carb entry and then delete both FPU and carb entries
+        ///     - If false: Will delete the entry directly as a standard carb deletion
+        /// - Note: This function handles three scenarios:
+        ///   1. Standard carb deletion (isFPUDeletion = false)
+        ///   2. FPU-only deletion (isFPUDeletion = true)
+        ///   3. Combined carb+FPU deletion (isFPUDeletion = true)
+        func deleteFromServices(_ treatmentObjectID: NSManagedObjectID, isFPUDeletion: Bool = false) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
             taskContext.transactionAuthor = "deleteCarbsFromServices"
 
             var carbEntry: CarbEntryStored?
+            var objectIDToDelete = treatmentObjectID
+
+            // For FPU deletions, first get the corresponding carb entry
+            if isFPUDeletion {
+                guard let correspondingEntry: (
+                    entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
+                    entryID: NSManagedObjectID?
+                ) = await handleFPUEntry(treatmentObjectID),
+                    let nsManagedObjectID = correspondingEntry.entryID
+                else { return }
+
+                objectIDToDelete = nsManagedObjectID
+            }
 
-            // Delete carbs or FPUs from Nightscout
+            // Delete entries from all services
             await taskContext.perform {
                 do {
-                    carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                    carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
                     guard let carbEntry = carbEntry else {
                         debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
                         return
                     }
 
-                    if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                    // Delete FPU related entries if they exist
+                    if let fpuID = carbEntry.fpuID {
                         // Delete Fat and Protein entries from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
 
@@ -152,29 +180,26 @@ extension DataTable {
                                 self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
                             }
                         }
-                    } else {
-                        // Delete carbs from Nightscout
-                        if let id = carbEntry.id, let entryDate = carbEntry.date {
-                            self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
-
-                            // Delete carbs from Apple Health
-                            if let sampleType = AppleHealthConfig.healthCarbObject {
-                                self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
-                            }
+                    }
+
+                    // Delete carb entries if they exist
+                    if let id = carbEntry.id, let entryDate = carbEntry.date {
+                        self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
 
-                            self.provider.deleteCarbsFromTidepool(
-                                withSyncId: id,
-                                carbs: Decimal(carbEntry.carbs),
-                                at: entryDate,
-                                enteredBy: CarbsEntry.local
-                            )
+                        // Delete carbs from Apple Health
+                        if let sampleType = AppleHealthConfig.healthCarbObject {
+                            self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
                         }
-                    }
 
+                        self.provider.deleteCarbsFromTidepool(
+                            withSyncId: id,
+                            carbs: Decimal(carbEntry.carbs),
+                            at: entryDate,
+                            enteredBy: CarbsEntry.local
+                        )
+                    }
                 } catch {
-                    debugPrint(
-                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
-                    )
+                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error.localizedDescription)")
                 }
             }
         }
@@ -246,6 +271,285 @@ extension DataTable {
                 }
             }
         }
+
+        // MARK: - Entry Management
+
+        /// Updates a carb/FPU entry with new values and handles the necessary cleanup and recreation of FPU entries
+        /// - Parameters:
+        ///   - treatmentObjectID: The ID of the entry to update
+        ///   - newCarbs: The new carbs value
+        ///   - newFat: The new fat value
+        ///   - newProtein: The new protein value
+        ///   - newNote: The new note text
+        ///   - newDate: The new date for the entry
+        func updateEntry(
+            _ treatmentObjectID: NSManagedObjectID,
+            newCarbs: Decimal,
+            newFat: Decimal,
+            newProtein: Decimal,
+            newNote: String,
+            newDate: Date
+        ) {
+            Task {
+                // Get original date from entry to re-create the entry later with the updated values and the same date
+                guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
+
+                // Deletion logic for carb and FPU entries
+                await deleteOldEntries(
+                    treatmentObjectID,
+                    originalEntry: originalEntry,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                await createNewEntries(
+                    originalDate: newDate,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                await syncWithServices()
+            }
+        }
+
+        private func createNewEntries(
+            originalDate: Date,
+            newCarbs: Decimal,
+            newFat: Decimal,
+            newProtein: Decimal,
+            newNote: String
+        ) async {
+            let newEntry = CarbsEntry(
+                id: UUID().uuidString,
+                createdAt: Date(),
+                actualDate: originalDate,
+                carbs: newCarbs,
+                fat: newFat,
+                protein: newProtein,
+                note: newNote,
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: newFat > 0 || newProtein > 0 ? UUID().uuidString : nil
+            )
+
+            // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
+            await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+        }
+
+        /// Deletes the old carb/ FPU entries and creates new ones with updated values
+        /// - Parameters:
+        ///   - treatmentObjectID: The ID of the entry to delete
+        ///   - originalDate: The original date to preserve
+        ///   - newCarbs: The new carbs value
+        ///   - newFat: The new fat value
+        ///   - newProtein: The new protein value
+        ///   - newNote: The new note text
+        private func deleteOldEntries(
+            _ treatmentObjectID: NSManagedObjectID,
+            originalEntry: (
+                entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?,
+                entryId: NSManagedObjectID
+            ),
+            newCarbs _: Decimal,
+            newFat _: Decimal,
+            newProtein _: Decimal,
+            newNote _: String
+        ) async {
+            if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
+                ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
+            {
+                // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
+                // Use fpuID
+                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+            } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
+                ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
+            {
+                // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
+                // Use fpuID
+                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+
+            } else {
+                // Delete just the carb entry since there are no carb equivalents
+                // Use NSManagedObjectID
+                await deleteCarbs(treatmentObjectID)
+            }
+        }
+
+        /// Retrieves the original entry values
+        /// - Parameter objectID: The ID of the entry
+        /// - Returns: A tuple of the old entry values and its original date and the objectID or nil
+        private func getOriginalEntryValues(_ objectID: NSManagedObjectID) async
+            -> (entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?, entryId: NSManagedObjectID)?
+        {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "updateContext"
+            context.transactionAuthor = "updateEntry"
+
+            return await context.perform {
+                do {
+                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored, let entryDate = entry.date
+                    else { return nil }
+
+                    return (
+                        entryValues: (date: entryDate, carbs: entry.carbs, fat: entry.fat, protein: entry.protein),
+                        entryId: entry.objectID
+                    )
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to get original date with error: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
+
+        /// Synchronizes the FPU/ Carb entry with all remote services in parallel
+        private func syncWithServices() async {
+            async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
+            async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
+            async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
+
+            _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
+        }
+
+        // MARK: - Entry Loading
+
+        /// Loads the values of a carb or FPU entry from Core Data
+        /// - Parameter objectID: The ID of the entry to load
+        /// - Returns: A tuple containing the entry's values, or nil if not found
+        func loadEntryValues(from objectID: NSManagedObjectID) async
+            -> (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?
+        {
+            let context = CoreDataStack.shared.persistentContainer.viewContext
+
+            return await context.perform {
+                do {
+                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored,
+                          let entryDate = entry.date
+                    else { return nil }
+
+                    return (
+                        carbs: Decimal(entry.carbs),
+                        fat: Decimal(entry.fat),
+                        protein: Decimal(entry.protein),
+                        note: entry.note ?? "",
+                        date: entryDate
+                    )
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error.localizedDescription)")
+                    return nil
+                }
+            }
+        }
+
+        // MARK: - FPU Entry Handling
+
+        /// Handles the loading of FPU entries based on their type
+        /// If the user taps on an FPU entry in the DataTable list, there are two cases:
+        /// - the User has entered this FPU entry WITH carbs
+        /// - the User has entered this FPU entry WITHOUT carbs
+        /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+        /// In the second case, we need to load the zero-carb entry that actually holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+        /// - Parameter objectID: The ID of the FPU entry
+        /// - Returns: A tuple containing the entry values and ID, or nil if not found
+        func handleFPUEntry(_ objectID: NSManagedObjectID) async
+            -> (
+                entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
+                entryID: NSManagedObjectID?
+            )?
+        {
+            // Case 1: FPU entry WITH carbs
+            if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
+                if let values = await loadEntryValues(from: correspondingCarbEntryID) {
+                    return (values, correspondingCarbEntryID)
+                }
+            }
+            // Case 2: FPU entry WITHOUT carbs
+            else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
+                if let values = await loadEntryValues(from: originalEntryID) {
+                    return (values, originalEntryID)
+                }
+            }
+            return nil
+        }
+
+        /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
+        /// This is used when the user has entered a FPU entry WITHOUT carbs.
+        /// - Parameter treatmentObjectID: The ID of the FPU entry
+        /// - Returns: The ID of the original entry, or nil if not found
+        func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "fpuContext"
+
+            return await context.perform {
+                do {
+                    // Get the fpuID from the selected entry
+                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                          let fpuID = selectedEntry.fpuID
+                    else { return nil }
+
+                    // Fetch the original zero-carb entry (non-FPU) with the same fpuID
+                    let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
+                    let request = CarbEntryStored.fetchRequest()
+                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                        NSPredicate(format: "isFPU == NO"),
+                        NSPredicate(format: "carbs == 0")
+                    ])
+                    request.fetchLimit = 1
+
+                    let originalEntry = try context.fetch(request).first
+                    debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
+                    return originalEntry?.objectID
+
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
+
+        /// Retrieves the corresponding carb entry for a given FPU entry.
+        /// This is used when the user has entered a carb entry WITH FPUs all at once.
+        /// - Parameter treatmentObjectID: The ID of the FPU entry
+        /// - Returns: The ID of the corresponding carb entry, or nil if not found
+        func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "carbContext"
+
+            return await context.perform {
+                do {
+                    // Get the fpuID from the selected entry
+                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                          let fpuID = selectedEntry.fpuID
+                    else { return nil }
+
+                    // Fetch the corresponding carb entry with the same fpuID
+                    let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
+                    let request = CarbEntryStored.fetchRequest()
+                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                        NSPredicate(format: "isFPU == NO"),
+                        NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
+                    ])
+                    request.fetchLimit = 1
+
+                    let correspondingCarbEntry = try context.fetch(request).first
+                    debugPrint(
+                        "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
+                    )
+                    return correspondingCarbEntry?.objectID
+
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
     }
 }
 

+ 229 - 0
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -0,0 +1,229 @@
+//
+//  CarbEntryEditorView.swift
+//  FreeAPS
+//
+//  Created by Marvin Polscheit on 15.01.25.
+//
+import CoreData
+import SwiftUI
+
+struct CarbEntryEditorView: View {
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    var state: DataTable.StateModel
+    let carbEntry: CarbEntryStored
+
+    /*
+     This is the objectID of the entry that the user is editing. It is NOT always the `carbEntry: CarbEntryStored` that we pass to the `CarbEntryEditorView`.
+     We need this because FPUs and carbs are treated completely different and that complicates the update process.
+     */
+    @State private var entryToEdit: NSManagedObjectID?
+
+    @State private var editedCarbs: Decimal
+    @State private var editedFat: Decimal
+    @State private var editedProtein: Decimal
+    @State private var editedNote: String
+    @State private var isFPU: Bool
+    @State private var editedDate: Date
+
+    init(state: DataTable.StateModel, carbEntry: CarbEntryStored) {
+        self.state = state
+        self.carbEntry = carbEntry
+        _editedCarbs = State(initialValue: 0) // gets updated in the task block
+        _editedFat = State(initialValue: 0) // gets updated in the task block
+        _editedProtein = State(initialValue: 0) // gets updated in the task block
+        _editedNote = State(initialValue: carbEntry.note ?? "")
+        _isFPU = State(initialValue: carbEntry.isFPU)
+        _entryToEdit = State(initialValue: nil)
+        _editedDate = State(initialValue: Date())
+    }
+
+    private var carbLimitExceeded: Bool {
+        editedCarbs > state.settingsManager.settings.maxCarbs
+    }
+
+    private var fatLimitExceeded: Bool {
+        editedFat > state.settingsManager.settings.maxFat
+    }
+
+    private var proteinLimitExceeded: Bool {
+        editedProtein > state.settingsManager.settings.maxProtein
+    }
+
+    private var limitExceeded: Bool {
+        carbLimitExceeded || fatLimitExceeded || proteinLimitExceeded
+    }
+
+    private var isButtonDisabled: Bool {
+        editedCarbs == 0 && editedFat == 0 && editedProtein == 0
+    }
+
+    private var buttonLabel: some View {
+        if carbLimitExceeded {
+            return Text("Max Carbs of \(state.settingsManager.settings.maxCarbs.description) g Exceeded")
+        } else if fatLimitExceeded {
+            return Text("Max Fat of \(state.settingsManager.settings.maxFat.description) g Exceeded")
+        } else if proteinLimitExceeded {
+            return Text("Max Protein of \(state.settingsManager.settings.maxProtein.description) g Exceeded")
+        }
+
+        return Text("Save and Update")
+    }
+
+    private var buttonBackgroundColor: Color {
+        var treatmentButtonBackground = Color(.systemBlue)
+        if limitExceeded {
+            treatmentButtonBackground = Color(.systemRed)
+        } else if isButtonDisabled {
+            treatmentButtonBackground = Color(.systemGray)
+        }
+
+        return treatmentButtonBackground
+    }
+
+    var stickyButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(
+                action: {
+                    guard let entryToEdit = entryToEdit else { return }
+
+                    state.updateEntry(
+                        entryToEdit,
+                        newCarbs: editedCarbs,
+                        newFat: editedFat,
+                        newProtein: editedProtein,
+                        newNote: editedNote,
+                        newDate: editedDate
+                    )
+                    dismiss()
+                }, label: {
+                    buttonLabel
+                        .font(.headline)
+                        .frame(maxWidth: .infinity, maxHeight: .infinity)
+                        .padding(10)
+                }
+            )
+            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+            .disabled(isButtonDisabled)
+            .background(buttonBackgroundColor)
+            .tint(.white)
+            .clipShape(RoundedRectangle(cornerRadius: 8))
+        }
+    }
+
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    HStack {
+                        Text("Carbs")
+                        TextFieldWithToolBar(
+                            text: $editedCarbs,
+                            placeholder: "0",
+                            keyboardType: .numberPad,
+                            numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                        )
+                        Text("g").foregroundStyle(.secondary)
+                    }
+
+                    if state.settingsManager.settings.useFPUconversion {
+                        HStack {
+                            Text("Protein")
+                            TextFieldWithToolBar(
+                                text: $editedProtein,
+                                placeholder: "0",
+                                keyboardType: .numberPad,
+                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+
+                        HStack {
+                            Text("Fat")
+                            TextFieldWithToolBar(
+                                text: $editedFat,
+                                placeholder: "0",
+                                keyboardType: .numberPad,
+                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+                    }
+
+                    HStack {
+                        Image(systemName: "square.and.pencil")
+                        TextFieldWithToolBarString(text: $editedNote, placeholder: "Note...", maxLength: 25)
+                    }
+                }.listRowBackground(Color.chart)
+
+                Section {
+                    DatePicker(
+                        "Time",
+                        selection: $editedDate,
+                        displayedComponents: [.date, .hourAndMinute]
+                    )
+                }
+            }
+            .safeAreaInset(
+                edge: .bottom,
+                spacing: 30
+            ) {
+                stickyButton
+            }
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Edit Meal")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Cancel") {
+                        dismiss()
+                    }
+                }
+            }
+        }
+        .task {
+            /*
+             User taps on a FPU entry in the DataTable list. There are two cases:
+             - the User has entered this FPU entry WITH carbs
+             - the User has entered this FPU entry WITHOUT carbs
+             In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+             In the second case, we need to load the zero-carb entry that actualy holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+             */
+            if carbEntry.isFPU {
+                if let result = await state.handleFPUEntry(carbEntry.objectID) {
+                    editedCarbs = result.entryValues?.carbs ?? 0
+                    editedFat = result.entryValues?.fat ?? 0
+                    editedProtein = result.entryValues?.protein ?? 0
+                    editedNote = result.entryValues?.note ?? ""
+                    entryToEdit = result.entryID
+                    editedDate = result.entryValues?.date ?? Date()
+                }
+                /*
+                 User taps on a carb entry in the DataTable list. There are again two cases which don't need explicit handling:
+                 - the User has only entered carbs
+                 - the User has entered carbs with FPU
+                 In both cases, we need to simply load the carb entry that holds all the necessary values for us. This is the entry we want to edit.
+                 */
+            } else {
+                if let values = await state.loadEntryValues(from: carbEntry.objectID) {
+                    editedCarbs = values.carbs
+                    editedFat = values.fat
+                    editedProtein = values.protein
+                    editedNote = values.note
+                    editedDate = values.date
+                    entryToEdit = carbEntry.objectID
+                }
+            }
+        }
+    }
+}

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

@@ -40,7 +40,7 @@ extension DataTable {
         @FetchRequest(
             entity: CarbEntryStored.entity(),
             sortDescriptors: [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)],
-            predicate: NSPredicate.predicateForOneDayAgo,
+            predicate: NSPredicate.carbsHistory,
             animation: .bouncy
         ) var carbEntryStored: FetchedResults<CarbEntryStored>
 
@@ -120,6 +120,11 @@ extension DataTable {
                 .sheet(isPresented: $showManualGlucose) {
                     addGlucoseView()
                 }
+                .sheet(isPresented: $state.showCarbEntryEditor) {
+                    if let carbEntry = state.carbEntryToEdit {
+                        CarbEntryEditorView(state: state, carbEntry: carbEntry)
+                    }
+                }
         }
 
         @ViewBuilder func addButton(_ action: @escaping () -> Void) -> some View {
@@ -584,20 +589,35 @@ extension DataTable {
                     action: {
                         alertCarbEntryToDelete = meal
 
-                        if !meal.isFPU {
+                        // meal is carb-only
+                        if meal.fpuID == nil {
                             alertTitle = "Delete Carbs?"
                             alertMessage = Formatter.dateFormatter
                                 .string(from: meal.date ?? Date()) + ", " +
                                 (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
                                 NSLocalizedString(" g", comment: "gram of carbs")
-                        } else {
-                            alertTitle = "Delete Carb Equivalents?"
-                            alertMessage = "All FPUs of the meal will be deleted."
+                        }
+                        // meal is complex-meal or fpu-only
+                        else {
+                            alertTitle = meal.isFPU ? "Delete Carbs Equivalents?" : "Delete Carbs?"
+                            alertMessage = "All FPUs and the carbs of the meal will be deleted."
                         }
 
                         isRemoveHistoryItemAlertPresented = true
                     }
                 ).tint(.red)
+
+                Button(
+                    "Edit",
+                    systemImage: "pencil",
+                    role: .none,
+                    action: {
+                        state.carbEntryToEdit = meal
+                        state.showCarbEntryEditor = true
+                    }
+                )
+                .tint(!state.settingsManager.settings.useFPUconversion && meal.isFPU ? Color(.systemGray4) : Color.blue)
+                .disabled(!state.settingsManager.settings.useFPUconversion && meal.isFPU)
             }
             .alert(
                 Text(NSLocalizedString(alertTitle, comment: "")),
@@ -611,7 +631,10 @@ extension DataTable {
                     }
                     let treatmentObjectID = carbEntryToDelete.objectID
 
-                    state.invokeCarbDeletionTask(treatmentObjectID)
+                    state.invokeCarbDeletionTask(
+                        treatmentObjectID,
+                        isFpuOrComplexMeal: carbEntryToDelete.isFPU || carbEntryToDelete.fat > 0 || carbEntryToDelete.protein > 0
+                    )
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))

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

@@ -3,7 +3,7 @@ import SwiftUI
 extension GlucoseNotificationSettings {
     final class StateModel: BaseStateModel<Provider> {
         @Published var glucoseBadge = false
-        @Published var glucoseNotificationsAlways = false
+        @Published var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
         @Published var useAlarmSound = false
         @Published var addSourceInfoToGlucoseNotifications = false
         @Published var lowGlucose: Decimal = 0
@@ -26,7 +26,7 @@ extension GlucoseNotificationSettings {
             subscribeSetting(\.notificationsAlgorithm, on: $notificationsAlgorithm) { notificationsAlgorithm = $0 }
 
             subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
-            subscribeSetting(\.glucoseNotificationsAlways, on: $glucoseNotificationsAlways) { glucoseNotificationsAlways = $0 }
+            subscribeSetting(\.glucoseNotificationsOption, on: $glucoseNotificationsOption) { glucoseNotificationsOption = $0 }
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
                 addSourceInfoToGlucoseNotifications = $0 }

+ 111 - 64
Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -42,6 +42,28 @@ extension GlucoseNotificationSettings {
             List {
                 SettingInputSection(
                     decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.useAlarmSound,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = "Play Alarm Sound"
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: "Play Alarm Sound",
+                    miniHint: "Alarm with every Trio notification.",
+                    verboseHint: VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: OFF").bold()
+                        Text(
+                            "This will cause a sound to be triggered by Trio notifications for Carbs Required, and Glucose Low/High Alarms."
+                        )
+                    }
+                )
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
                     booleanValue: $state.notificationsPump,
                     shouldDisplayHint: $shouldDisplayHint,
                     selectedVerboseHint: Binding(
@@ -161,75 +183,94 @@ extension GlucoseNotificationSettings {
                     miniHint: "Show your current glucose on Trio app icon.",
                     verboseHint: VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
-                        Text("This will add your current glucose on the top right of your Trio icon as a red notification badge.")
+                        Text(
+                            "This will add your current glucose on the top right of your Trio icon as a red notification badge. Changing setting takes effect on next Glucose reading."
+                        )
                     },
                     headerText: "Various Glucose Notifications"
                 )
 
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.glucoseNotificationsAlways,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Always Notify Glucose"
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Always Notify Glucose",
-                    miniHint: "Trigger a notification every time your glucose is updated.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("A notification will be triggered every time your glucose is updated in Trio.")
-                    }
-                )
+                Section {
+                    VStack {
+                        Picker(
+                            selection: $state.glucoseNotificationsOption,
+                            label: Text("Glucose Notifications")
+                        ) {
+                            ForEach(GlucoseNotificationsOption.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
 
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.useAlarmSound,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Play Alarm Sound"
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Play Alarm Sound",
-                    miniHint: "Alarm with every Trio notification.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("This will cause a sound to be triggered by every Trio notification.")
-                    }
-                )
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose glucose notifications option. See hint for more details."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = "Glucose Notifications"
+                                    selectedVerboseHint =
+                                        AnyView(
+                                            VStack(alignment: .leading, spacing: 10) {
+                                                Text(
+                                                    "Set the Glucose Notifications Option. Descriptions for each option found below."
+                                                )
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Disabled:").bold()
+                                                    Text("No Glucose Notificatitons will be triggered.")
+                                                }
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Always:").bold()
+                                                    Text(
+                                                        "A notification will be triggered every time your glucose is updated in Trio."
+                                                    )
+                                                }
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Only Alarm Limits:").bold()
+                                                    Text(
+                                                        "A notification will be triggered only when glucose levels are below the LOW limit or above the HIGH limit, as specified in Glucose Alarm Limits below."
+                                                    )
+                                                }
+                                            }
+                                        )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
 
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.addSourceInfoToGlucoseNotifications,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Add Glucose Source to Alarm"
+                if state.glucoseNotificationsOption != GlucoseNotificationsOption.disabled {
+                    self.lowAndHighGlucoseAlertSection
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.addSourceInfoToGlucoseNotifications,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0.map { AnyView($0) }
+                                hintLabel = "Add Glucose Source to Alarm"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Add Glucose Source to Alarm",
+                        miniHint: "Source of the glucose reading will be added to the notification.",
+                        verboseHint: VStack(alignment: .leading, spacing: 10) {
+                            Text("Default: OFF").bold()
+                            Text("The source of the glucose reading will be added to the notification.")
                         }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Add Glucose Source to Alarm",
-                    miniHint: "Source of the glucose reading will be added to the notification.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("The source of the glucose reading will be added to the notification.")
-                    }
-                )
-
-                self.lowAndHighGlucoseAlertSection
+                    )
+                }
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {
@@ -336,8 +377,14 @@ extension GlucoseNotificationSettings {
                                 hintLabel = "Low and High Glucose Alarm Limits"
                                 selectedVerboseHint =
                                     AnyView(VStack(alignment: .leading, spacing: 10) {
-                                        Text("Low Default: 70 mg/dL").bold()
-                                        Text("High Default: 180 mg/dL").bold()
+                                        let low: Decimal = 70
+                                        let high: Decimal = 180
+                                        let labelLow = (state.units == .mgdL ? low.description : low.formattedAsMmolL) + " " +
+                                            state.units.rawValue
+                                        let labelHigh = (state.units == .mgdL ? high.description : high.formattedAsMmolL) + " " +
+                                            state.units.rawValue
+                                        Text("Low Default: " + labelLow).bold()
+                                        Text("High Default: " + labelHigh).bold()
                                         VStack(alignment: .leading, spacing: 10) {
                                             Text(
                                                 "These two settings determine the range outside of which you will be notified via push notifications."

+ 0 - 3
Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift

@@ -78,9 +78,6 @@ extension Home.StateModel {
             )
         }
 
-        //TODO: - remove this after bug is fixed
-        debugPrint("\(DebuggingIdentifiers.inProgress) printing target profiles: \(targetProfiles)")
-        
         return targetProfiles
     }
 }

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

@@ -580,6 +580,7 @@ extension Home.StateModel:
         highGlucose = settingsManager.settings.high
         Task {
             await getCurrentGlucoseTarget()
+            await setupGlucoseTargets()
         }
         hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
         glucoseColorScheme = settingsManager.settings.glucoseColorScheme

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

@@ -50,7 +50,7 @@ struct CarbView: ChartContent {
         ForEach(fpuData, id: \.id) { fpu in
             let fpuAmount = fpu.carbs
             let size = (MainChartHelper.Config.fpuSize + CGFloat(fpuAmount) * MainChartHelper.Config.carbsScale) * 1.8
-            let yPosition = minValue
+            let yPosition = minValue // value is parsed to mmol/L when passed into struct based on user settings
 
             PointMark(
                 x: .value("Time", fpu.date ?? Date(), unit: .second),

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

@@ -157,7 +157,8 @@ extension MainChartView {
                     units: state.units,
                     carbData: state.carbsFromPersistence,
                     fpuData: state.fpusFromPersistence,
-                    minValue: state.minYAxisValue
+                    minValue: units == .mgdL ? state.minYAxisValue : state.minYAxisValue
+                        .asMmolL
                 )
 
                 ForecastView(

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

@@ -91,7 +91,7 @@ struct PumpView: View {
                         Image(systemName: "battery.100")
                             .font(.callout)
                             .foregroundStyle(batteryColor)
-                        Text("\(Int(battery.first?.percent ?? 100)) %")
+                        Text("\(Formatter.integerFormatter.string(for: battery.first?.percent ?? 100) ?? "100") %")
                             .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
                 }

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

@@ -46,7 +46,14 @@ extension ISFEditor {
                                     state.shouldDisplaySaving = false
                                 }
                             } label: {
-                                Text(state.shouldDisplaySaving ? "Saving..." : "Save").padding(10)
+                                HStack {
+                                    if state.shouldDisplaySaving {
+                                        ProgressView().padding(.trailing, 10)
+                                    }
+                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                                }
+                                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                                .padding(10)
                             }
                         }
                         .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)

+ 36 - 39
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -57,25 +57,6 @@ extension PumpConfig {
                                         Spacer()
                                         Button(
                                             action: {
-                                                hintLabel = "Pump Pairing to Trio"
-                                                selectedVerboseHint =
-                                                    AnyView(
-                                                        VStack(alignment: .leading, spacing: 10) {
-                                                            Text(
-                                                                "Current Pump Models Supported:"
-                                                            )
-                                                            VStack(alignment: .leading) {
-                                                                Text("• Medtronic")
-                                                                Text("• Omnipod Eros")
-                                                                Text("• Omnipod Dash")
-                                                                Text("• Dana (RS/-i)")
-                                                                Text("• Pump Simulator")
-                                                            }
-                                                            Text(
-                                                                "Note: If using a pump simulator, you will not have continuous readings from the CGM in Trio. Using a pump simulator is only advisable for becoming familiar with the app user interface. It will not give you insight on how the algorithm will respond."
-                                                            )
-                                                        }
-                                                    )
                                                 shouldDisplayHint.toggle()
                                             },
                                             label: {
@@ -97,12 +78,46 @@ extension PumpConfig {
                 .navigationTitle("Insulin Pump")
                 .navigationBarTitleDisplayMode(.automatic)
                 .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
+                .sheet(isPresented: $state.setupPump) {
+                    if let pumpManager = state.provider.apsManager.pumpManager {
+                        PumpSettingsView(
+                            pumpManager: pumpManager,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            completionDelegate: state,
+                            setupDelegate: state
+                        )
+                    } else {
+                        PumpSetupView(
+                            pumpType: state.setupPumpType,
+                            pumpInitialSettings: state.initialSettings,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            completionDelegate: state,
+                            setupDelegate: state
+                        )
+                    }
+                }
                 .sheet(isPresented: $shouldDisplayHint) {
                     SettingInputHintView(
                         hintDetent: $hintDetent,
                         shouldDisplayHint: $shouldDisplayHint,
-                        hintLabel: hintLabel ?? "",
-                        hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                        hintLabel: "Pump Pairing to Trio",
+                        hintText: AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Current Pump Models Supported:"
+                                )
+                                VStack(alignment: .leading) {
+                                    Text("• Medtronic")
+                                    Text("• Omnipod Eros")
+                                    Text("• Omnipod Dash")
+                                    Text("• Dana (RS/-i)")
+                                    Text("• Pump Simulator")
+                                }
+                                Text(
+                                    "Note: If using a pump simulator, you will not have continuous readings from the CGM in Trio. Using a pump simulator is only advisable for becoming familiar with the app user interface. It will not give you insight on how the algorithm will respond."
+                                )
+                            }
+                        ),
                         sheetTitle: "Help"
                     )
                 }
@@ -114,24 +129,6 @@ extension PumpConfig {
                     Button("Pump Simulator") { state.addPump(.simulator) }
                 } message: { Text("Select Pump Model") }
             }
-            .sheet(isPresented: $state.setupPump) {
-                if let pumpManager = state.provider.apsManager.pumpManager {
-                    PumpSettingsView(
-                        pumpManager: pumpManager,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        completionDelegate: state,
-                        setupDelegate: state
-                    )
-                } else {
-                    PumpSetupView(
-                        pumpType: state.setupPumpType,
-                        pumpInitialSettings: state.initialSettings,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        completionDelegate: state,
-                        setupDelegate: state
-                    )
-                }
-            }
         }
     }
 }

+ 6 - 1
Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -87,7 +87,12 @@ extension PumpConfig {
             case let .createdAndOnboarded(pumpManagerUI):
                 debug(.default, "Pump manager  created and onboarded")
                 setupDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManagerUI)
-                return UIViewController()
+                var vc = pumpManagerUI.settingsViewController(
+                    bluetoothProvider: bluetoothManager,
+                    pumpManagerOnboardingDelegate: setupDelegate
+                )
+                vc.completionDelegate = completionDelegate
+                return vc
             }
         }
 

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

@@ -306,7 +306,7 @@ extension SMBSettings {
                         }
                         VStack(alignment: .leading, spacing: 10) {
                             Text(
-                                "Warning: Increasing this value above 90 minutes may impact Trio's ability to effectively zero temp and prevent lows."
+                                "Warning: Increasing this value above 60 minutes may impact Trio's ability to effectively zero temp and prevent lows."
                             ).bold()
                             Text("Note: UAM SMBs must be enabled to use this limit.")
                         }

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

@@ -219,13 +219,13 @@ enum SettingItems {
             title: "Trio Notifications",
             view: .glucoseNotificationSettings,
             searchContents: [
+                "Play Alarm Sound",
                 "Always Notify Pump",
                 "Always Notify CGM",
                 "Always Notify Carb",
                 "Always Notify Algorithm",
                 "Show Glucose App Badge",
-                "Always Notify Glucose",
-                "Play Alarm Sound",
+                "Glucose Notifications",
                 "Add Glucose Source to Alarm",
                 "Low Glucose Alarm Limit",
                 "High Glucose Alarm Limit"

+ 2 - 7
Trio/Sources/Modules/Stat/View/StatsView.swift

@@ -128,13 +128,8 @@ struct StatsView: View {
     var hba1c: some View {
         HStack(spacing: 50) {
             let useUnit: GlucoseUnits = {
-                if units == .mmolL && hbA1cDisplayUnit == .mmolMol {
-                    return .mgdL
-                } else if (units == .mgdL && hbA1cDisplayUnit == .mmolMol) || units == .mmolL {
-                    return .mmolL
-                } else {
-                    return .mgdL
-                }
+                if hbA1cDisplayUnit == .mmolMol { return .mmolL }
+                else { return .mgdL }
             }()
 
             let hba1cs = glucoseStats()

+ 22 - 22
Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -31,30 +31,30 @@ extension TargetsEditor {
 
                 Group {
                     HStack {
-                        HStack {
-                            Button {
-                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                                impactHeavy.impactOccurred()
-                                state.save()
-
-                                // deactivate saving display after 1.25 seconds
-                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                    state.shouldDisplaySaving = false
+                        Button(action: {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+
+                            // deactivate saving display after 1.25 seconds
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                state.shouldDisplaySaving = false
+                            }
+                        }, label: {
+                            HStack {
+                                if state.shouldDisplaySaving {
+                                    ProgressView().padding(.trailing, 10)
                                 }
-                            } label: {
-                                HStack {
-                                    if state.shouldDisplaySaving {
-                                        ProgressView().padding(.trailing, 10)
-                                    }
-                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                                }.padding(10)
+                                Text(state.shouldDisplaySaving ? "Saving..." : "Save")
                             }
-                        }
-                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
-                        .disabled(shouldDisableButton)
-                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
-                        .tint(.white)
-                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                            .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                            .padding(10)
+                        })
+                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                            .disabled(shouldDisableButton)
+                            .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                            .tint(.white)
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
                     }
                 }.padding(5)
             }

+ 2 - 1
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -553,7 +553,8 @@ extension Treatments {
                 protein: protein,
                 note: note,
                 enteredBy: CarbsEntry.local,
-                isFPU: false, fpuID: UUID().uuidString
+                isFPU: false,
+                fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
             )]
             await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
 

+ 1 - 1
Trio/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift

@@ -177,7 +177,7 @@ struct MealPresetView: View {
     }
 
     private var noPresetChosen: Bool {
-        state.selection == nil || carbs == 0 // || (state.useFPUconversion && (fat == 0 || protein == 0))
+        state.selection == nil || state.summation.isEmpty
     }
 
     @ViewBuilder private func dishInfos() -> some View {

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

@@ -79,31 +79,31 @@ extension Treatments {
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
                 HStack {
-                    Text("Fat")
+                    Text("Protein")
+
                     TextFieldWithToolBar(
-                        text: $state.fat,
+                        text: $state.protein,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
                         previousTextField: { focusOnPreviousTextField(index: 2) },
                         nextTextField: { focusOnNextTextField(index: 2) }
-                    ).focused($focusedField, equals: .fat)
+                    ).focused($focusedField, equals: .protein)
                     Text("g").foregroundColor(.secondary)
                 }
 
                 Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
 
                 HStack {
-                    Text("Protein")
-
+                    Text("Fat")
                     TextFieldWithToolBar(
-                        text: $state.protein,
+                        text: $state.fat,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
                         previousTextField: { focusOnPreviousTextField(index: 3) },
                         nextTextField: { focusOnNextTextField(index: 3) }
-                    ).focused($focusedField, equals: .protein)
+                    ).focused($focusedField, equals: .fat)
                     Text("g").foregroundColor(.secondary)
                 }
             }
@@ -328,15 +328,17 @@ extension Treatments {
                         Text("Close")
                     }
                 }
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: {
-                        showPresetSheet = true
-                    }, label: {
-                        HStack {
-                            Text("Presets")
-                            Image(systemName: "plus")
-                        }
-                    })
+                if state.displayPresets {
+                    ToolbarItem(placement: .topBarTrailing) {
+                        Button(action: {
+                            showPresetSheet = true
+                        }, label: {
+                            HStack {
+                                Text("Presets")
+                                Image(systemName: "plus")
+                            }
+                        })
+                    }
                 }
             })
             .onAppear {
@@ -408,7 +410,7 @@ extension Treatments {
 
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
-                return Text("Max Bolus of \(state.maxBolus.description) U E== 0xceeded")
+                return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
             } else if externalBolusLimitExceeded {
                 return Text("Max External Bolus of \(state.maxExternal.description) U Exceeded")
             } else if carbLimitExceeded {

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

@@ -19,7 +19,7 @@ struct WatchConfigGarminView: View {
     }
 
     var body: some View {
-        List {
+        Form {
             Section(
                 header: Text("Garmin Configuration"),
                 content:
@@ -43,13 +43,6 @@ struct WatchConfigGarminView: View {
                             Spacer()
                             Button(
                                 action: {
-                                    hintLabel = "Add Device"
-                                    selectedVerboseHint =
-                                        AnyView(
-                                            Text(
-                                                "Add Garmin Device to Trio. Please look at the docs to see which devices are supported."
-                                            )
-                                        )
                                     shouldDisplayHint.toggle()
                                 },
                                 label: {
@@ -64,14 +57,17 @@ struct WatchConfigGarminView: View {
             ).listRowBackground(Color.chart)
 
             if !state.devices.isEmpty {
-                Section(header: Text("Garmin Watch")) {
-                    List {
-                        ForEach(state.devices, id: \.uuid) { device in
-                            Text(device.friendlyName)
+                Section(
+                    header: Text("Garmin Watch"),
+                    content: {
+                        List {
+                            ForEach(state.devices, id: \.uuid) { device in
+                                Text(device.friendlyName)
+                            }
+                            .onDelete(perform: onDelete)
                         }
-                        .onDelete(perform: onDelete)
                     }
-                }.listRowBackground(Color.chart)
+                ).listRowBackground(Color.chart)
             }
         }
         .listSectionSpacing(sectionSpacing)
@@ -79,8 +75,10 @@ struct WatchConfigGarminView: View {
             SettingInputHintView(
                 hintDetent: $hintDetent,
                 shouldDisplayHint: $shouldDisplayHint,
-                hintLabel: hintLabel ?? "",
-                hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                hintLabel: "Add Device",
+                hintText: Text(
+                    "Add Garmin Device to Trio. Please look at the docs to see which devices are supported."
+                ),
                 sheetTitle: "Help"
             )
         }

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

@@ -60,7 +60,11 @@ final class BaseRouter: Router {
         case .carb:
             guard settings.notificationsCarb else { return false }
         case .glucose:
-            guard settings.glucoseNotificationsAlways else { return false }
+            guard (
+                message.type == .warning &&
+                    settings.glucoseNotificationsOption == GlucoseNotificationsOption.onlyAlarmLimits
+            ) ||
+                settings.glucoseNotificationsOption == GlucoseNotificationsOption.alwaysEveryCGM else { return false }
         case .algorithm:
             guard settings.notificationsAlgorithm else { return false }
         case .misc:

+ 19 - 18
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -245,26 +245,27 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             for allSamples in carbs {
                 guard let id = allSamples.id else { continue }
                 let fpuID = allSamples.fpuID ?? id
-
                 let startDate = allSamples.actualDate ?? Date()
 
-                // Carbs Sample
+                // Carbs Sample (only if value is greater than 0)
                 let carbValue = allSamples.carbs
-                let carbSample = HKQuantitySample(
-                    type: carbSampleType,
-                    quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
-                    start: startDate,
-                    end: startDate,
-                    metadata: [
-                        HKMetadataKeyExternalUUID: id,
-                        HKMetadataKeySyncIdentifier: id,
-                        HKMetadataKeySyncVersion: 1
-                    ]
-                )
-                samples.append(carbSample)
+                if carbValue > 0 {
+                    let carbSample = HKQuantitySample(
+                        type: carbSampleType,
+                        quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
+                        start: startDate,
+                        end: startDate,
+                        metadata: [
+                            HKMetadataKeyExternalUUID: id,
+                            HKMetadataKeySyncIdentifier: id,
+                            HKMetadataKeySyncVersion: 1
+                        ]
+                    )
+                    samples.append(carbSample)
+                }
 
-                // Fat Sample (if available)
-                if let fatValue = allSamples.fat {
+                // Fat Sample (only if value is greater than 0)
+                if let fatValue = allSamples.fat, fatValue > 0 {
                     let fatSample = HKQuantitySample(
                         type: fatSampleType,
                         quantity: HKQuantity(unit: .gram(), doubleValue: Double(fatValue)),
@@ -279,8 +280,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                     samples.append(fatSample)
                 }
 
-                // Protein Sample (if available)
-                if let proteinValue = allSamples.protein {
+                // Protein Sample (only if value is greater than 0)
+                if let proteinValue = allSamples.protein, proteinValue > 0 {
                     let proteinSample = HKQuantitySample(
                         type: proteinSampleType,
                         quantity: HKQuantity(unit: .gram(), doubleValue: Double(proteinValue)),

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

@@ -69,7 +69,7 @@ extension TrioRemoteControl {
             note: "Remote meal command",
             enteredBy: CarbsEntry.local,
             isFPU: false,
-            fpuID: nil
+            fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
 
         await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)

+ 7 - 11
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -128,7 +128,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private func addAppBadge(glucose: Int?) {
         guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
             DispatchQueue.main.async {
-                self.center.setBadgeCount(-1) { error in
+                self.center.setBadgeCount(0) { error in
                     guard let error else {
                         return
                     }
@@ -270,8 +270,6 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
             addAppBadge(glucose: (glucoseObjects.first?.glucose).map { Int($0) })
 
-            guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
-
             var titles: [String] = []
             var notificationAlarm = false
             var messageType = MessageType.info
@@ -421,24 +419,22 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             trigger: trigger,
             action: action
         )
-        if alertPermissionsChecker.notificationsDisabled {
-            router.alertMessage.send(messageCont)
-            return
-        }
-        guard router.allowNotify(messageCont, settingsManager.settings) else { return }
-
         var alertIdentifier = identifier.rawValue
         alertIdentifier = identifier == .pumpNotification ? alertIdentifier + content
             .title : (identifier == .alertMessageNotification ? alertIdentifier + content.body : alertIdentifier)
-        let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
-
         if deleteOld {
             DispatchQueue.main.async {
                 self.center.removeDeliveredNotifications(withIdentifiers: [alertIdentifier])
                 self.center.removePendingNotificationRequests(withIdentifiers: [alertIdentifier])
             }
         }
+        if alertPermissionsChecker.notificationsDisabled {
+            router.alertMessage.send(messageCont)
+            return
+        }
+        guard router.allowNotify(messageCont, settingsManager.settings) else { return }
 
+        let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
             self.center.add(request) { error in
                 if let error = error {

+ 11 - 4
oref0_source_version.txt

@@ -1,9 +1,16 @@
-oref0 branch: dev - git version: 22603e2
+oref0 branch: tdd-pumpdataCheck - git version: 46deecb
 
 Last commits:
-22603e2 skip_NetralTemps only if SMB's somehow disabled
-0ff47a3 fix weightedAverage, always calculate TDD
-e274bb0 revert to standard HBT calculation
+46deecb remove dynisf check on pumpdata calc
+2f258b2 Merge pull request #36 from mountrcg/fixTDDcheck
+4998a09 fix condition  to use weightedAverage as TDD
+ff54c14 Merge pull request #35 from mountrcg/skipNTfix
+bbdf258 Merge pull request #34 from mountrcg/TToref-reset
+6e4c8ce Merge pull request #31 from simonp22/calc_tdd
+c5f9a6a Use weightedAverage as tdd if weightPercentage > 1
+7153b43 skip_NetralTemps only if SMB's somehow disabled
+e0caaa0 revert to standard HBT calculation
+06ca64b Always calculate TDD
 363fd11 Merge pull request #28 from bjornoleh/harmonise_defaults
 2d695e1 index.js: set enableUAM to false, and remove whitespace in L11
 8f5f820 Harmonise profile defaults with openaps/oref0

+ 1 - 2
trio-oref/lib/determine-basal/determine-basal.js

@@ -383,7 +383,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
         console.log("Pumphistory is empty!");
         dynISFenabled = false;
         enableDynamicCR = false;
-    } else if (dynISFenabled) {
+    } else {
         let phLastEntry = pumphistory.length - 1;
         var endDate = new Date(pumphistory[phLastEntry].timestamp);
         var startDate = new Date(pumphistory[0].timestamp);
@@ -559,7 +559,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
 
     } else { tddReason = ", TDD: Not enough pumpData (< 21h)"; }
 
-
     var tdd_before = tdd;
 
     // -------------------- END OF TDD ----------------------------------------------------