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

Merge pull request #55 from dnzxy/tidepool

polscm32 1 год назад
Родитель
Сommit
14dbbbda85
25 измененных файлов с 1678 добавлено и 903 удалено
  1. 13 34
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  2. 81 6
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  3. 163 3
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  4. 109 0
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  5. 3 2
      FreeAPS/Sources/Models/BloodGlucose.swift
  6. 1 1
      FreeAPS/Sources/Models/CarbsEntry.swift
  7. 7 2
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  8. 36 34
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  9. 120 45
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  10. 18 18
      FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  11. 16 14
      FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift
  12. 7 2
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  13. 14 1
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  14. 1 1
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  15. 29 5
      FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift
  16. 500 487
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  17. 11 1
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  18. 474 245
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  19. 2 0
      Model/Classes+Properties/CarbEntryStored+CoreDataProperties.swift
  20. 2 0
      Model/Classes+Properties/GlucoseStored+CoreDataProperties.swift
  21. 2 0
      Model/Classes+Properties/PumpEventStored+CoreDataProperties.swift
  22. 19 1
      Model/Helper/CarbEntryStored+helper.swift
  23. 30 0
      Model/Helper/GlucoseStored+helper.swift
  24. 10 0
      Model/Helper/PumpEvent+helper.swift
  25. 10 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

+ 13 - 34
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -28,6 +28,7 @@ extension FetchGlucoseManager {
 
 
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
+
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var tidepoolService: TidepoolManager!
     @Injected() var tidepoolService: TidepoolManager!
@@ -165,18 +166,16 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
     /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
     public func refreshCGM() {
     public func refreshCGM() {
         debug(.deviceManager, "refreshCGM by pump")
         debug(.deviceManager, "refreshCGM by pump")
-        // updateGlucoseSource(cgmGlucoseSourceType: settingsManager.settings.cgm, cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier)
 
 
-        Publishers.CombineLatest3(
+        Publishers.CombineLatest(
             Just(glucoseStorage.syncDate()),
             Just(glucoseStorage.syncDate()),
-            healthKitManager.fetch(nil),
             glucoseSource.fetchIfNeeded()
             glucoseSource.fetchIfNeeded()
         )
         )
         .eraseToAnyPublisher()
         .eraseToAnyPublisher()
         .receive(on: processQueue)
         .receive(on: processQueue)
-        .sink { syncDate, glucoseFromHealth, glucose in
+        .sink { syncDate, glucose in
             debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
             debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
-            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
+            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose)
         }
         }
         .store(in: &lifetime)
         .store(in: &lifetime)
     }
     }
@@ -210,11 +209,10 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
         }
     }
     }
 
 
-    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
+    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) {
         // calibration add if required only for sensor
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
         let newGlucose = overcalibrate(entries: glucose)
 
 
-        let allGlucose = newGlucose + glucoseFromHealth
         var filteredByDate: [BloodGlucose] = []
         var filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
 
@@ -226,7 +224,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             backGroundFetchBGTaskID = .invalid
             backGroundFetchBGTaskID = .invalid
         }
         }
 
 
-        guard allGlucose.isNotEmpty else {
+        guard newGlucose.isNotEmpty else {
             if let backgroundTask = backGroundFetchBGTaskID {
             if let backgroundTask = backGroundFetchBGTaskID {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 backGroundFetchBGTaskID = .invalid
                 backGroundFetchBGTaskID = .invalid
@@ -234,11 +232,11 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             return
             return
         }
         }
 
 
-        filteredByDate = allGlucose.filter { $0.dateString > syncDate }
+        filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
 
         guard filtered.isNotEmpty else {
         guard filtered.isNotEmpty else {
-            // end of the BG tasks
+            // end of the Background tasks
             if let backgroundTask = backGroundFetchBGTaskID {
             if let backgroundTask = backGroundFetchBGTaskID {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 backGroundFetchBGTaskID = .invalid
                 backGroundFetchBGTaskID = .invalid
@@ -265,24 +263,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
 
         deviceDataManager.heartbeat(date: Date())
         deviceDataManager.heartbeat(date: Date())
 
 
-        Task.detached {
-            await self.nightscoutManager.uploadGlucose()
-            await self.tidepoolService.uploadGlucose(device: self.cgmManager?.cgmManagerStatus.device)
-        }
-
-        let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
-
-        guard glucoseForHealth.isNotEmpty else {
-            // end of the BG tasks
-            if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundFetchBGTaskID = .invalid
-            }
-            return
-        }
-        healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
-
-        // end of the BG tasks
+        // End of the Background tasks
         if let backgroundTask = backGroundFetchBGTaskID {
         if let backgroundTask = backGroundFetchBGTaskID {
             UIApplication.shared.endBackgroundTask(backgroundTask)
             UIApplication.shared.endBackgroundTask(backgroundTask)
             backGroundFetchBGTaskID = .invalid
             backGroundFetchBGTaskID = .invalid
@@ -303,17 +284,15 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             }
             }
             .sink { glucose in
             .sink { glucose in
                 debug(.nightscout, "FetchGlucoseManager callback sensor")
                 debug(.nightscout, "FetchGlucoseManager callback sensor")
-                Publishers.CombineLatest3(
+                Publishers.CombineLatest(
                     Just(glucose),
                     Just(glucose),
-                    Just(self.glucoseStorage.syncDate()),
-                    self.healthKitManager.fetch(nil)
+                    Just(self.glucoseStorage.syncDate())
                 )
                 )
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
-                .sink { newGlucose, syncDate, glucoseFromHealth in
+                .sink { newGlucose, syncDate in
                     self.glucoseStoreAndHeartDecision(
                     self.glucoseStoreAndHeartDecision(
                         syncDate: syncDate,
                         syncDate: syncDate,
-                        glucose: newGlucose,
-                        glucoseFromHealth: glucoseFromHealth
+                        glucose: newGlucose
                     )
                     )
                 }
                 }
                 .store(in: &self.lifetime)
                 .store(in: &self.lifetime)

+ 81 - 6
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -17,6 +17,8 @@ protocol CarbsStorage {
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
+    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
 }
 }
 
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -44,8 +46,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
         }
 
 
-        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+
+        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
     }
 
 
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
@@ -116,7 +119,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      */
      */
     private func processFPU(
     private func processFPU(
-        entries _: [CarbsEntry],
+        entries: [CarbsEntry],
         fat: Decimal,
         fat: Decimal,
         protein: Decimal,
         protein: Decimal,
         createdAt: Date,
         createdAt: Date,
@@ -145,7 +148,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var numberOfEquivalents = carbEquivalents / carbEquivalentSize
         var numberOfEquivalents = carbEquivalents / carbEquivalentSize
 
 
         var useDate = actualDate ?? createdAt
         var useDate = actualDate ?? createdAt
-        let fpuID = UUID().uuidString
+        let fpuID = entries.first?.fpuID ?? UUID().uuidString
         var futureCarbArray = [CarbsEntry]()
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
         var firstIndex = true
 
 
@@ -162,7 +165,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 fat: 0,
                 fat: 0,
                 protein: 0,
                 protein: 0,
                 note: nil,
                 note: nil,
-                enteredBy: CarbsEntry.manual, isFPU: true,
+                enteredBy: CarbsEntry.manual,
+                isFPU: true,
                 fpuID: fpuID
                 fpuID: fpuID
             )
             )
             futureCarbArray.append(eachCarbEntry)
             futureCarbArray.append(eachCarbEntry)
@@ -203,6 +207,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.id = UUID()
             newItem.id = UUID()
             newItem.isFPU = false
             newItem.isFPU = false
             newItem.isUploadedToNS = areFetchedFromRemote ? true : false
             newItem.isUploadedToNS = areFetchedFromRemote ? true : false
+            newItem.isUploadedToHealth = false
+            newItem.isUploadedToTidepool = false
+
+            if entry.fat != nil, entry.protein != nil, let fpuId = entry.fpuID {
+                newItem.fpuID = UUID(uuidString: fpuId)
+            }
 
 
             do {
             do {
                 guard self.coredataContext.hasChanges else { return }
                 guard self.coredataContext.hasChanges else { return }
@@ -214,8 +224,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
     }
 
 
     private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
     private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
-        let commonFPUID =
-            UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
+        let commonFPUID = UUID(
+            uuidString: entries.first?.fpuID ?? UUID()
+                .uuidString
+        ) // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
         var entrySlice = ArraySlice(entries) // convert to ArraySlice
         var entrySlice = ArraySlice(entries) // convert to ArraySlice
         let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
         let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
             guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
             guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
@@ -229,6 +241,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             carbEntry.fpuID = commonFPUID
             carbEntry.fpuID = commonFPUID
             carbEntry.isFPU = true
             carbEntry.isFPU = true
             carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
             carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
+            // do NOT set Health and Tidepool flags to ensure they will NOT be uploaded
             return false // return false to continue
             return false // return false to continue
         }
         }
         await coredataContext.perform {
         await coredataContext.perform {
@@ -403,4 +416,66 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             }
             }
         }
         }
     }
     }
+
+    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToHealth,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: Decimal(result.fat),
+                    protein: Decimal(result.protein),
+                    note: result.note,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: result.isFPU,
+                    fpuID: result.fpuID?.uuidString
+                )
+            }
+        }
+    }
+
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.carbsNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false
+        )
+
+        guard let carbEntries = results as? [CarbEntryStored] else {
+            return []
+        }
+
+        return await coredataContext.perform {
+            return carbEntries.map { result in
+                CarbsEntry(
+                    id: result.id?.uuidString,
+                    createdAt: result.date ?? Date(),
+                    actualDate: result.date,
+                    carbs: Decimal(result.carbs),
+                    fat: nil,
+                    protein: nil,
+                    note: result.note,
+                    enteredBy: CarbsEntry.manual,
+                    isFPU: nil,
+                    fpuID: nil
+                )
+            }
+        }
+    }
 }
 }

+ 163 - 3
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -2,6 +2,7 @@ import AVFAudio
 import Combine
 import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import LoopKit
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
@@ -17,7 +18,12 @@ protocol GlucoseStorage {
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
+    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
+    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
+    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
     var alarm: GlucoseAlarm? { get }
     var alarm: GlucoseAlarm? { get }
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
 }
 
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -88,6 +94,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.date = entry.dateString
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
+                        glucoseEntry.isUploadedToHealth = false /// the value is not uploaded to Health (yet)
+                        glucoseEntry.isUploadedToTidepool = false /// the value is not uploaded to Tidepool (yet)
                         return false // Continue processing
                         return false // Continue processing
                     }
                     }
                 )
                 )
@@ -241,7 +249,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
     }
 
 
     // Fetch glucose that is not uploaded to Nightscout yet
     // Fetch glucose that is not uploaded to Nightscout yet
-    /// Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
+    /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
     func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
@@ -272,7 +280,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
     }
 
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     // Fetch manual glucose that is not uploaded to Nightscout yet
-    /// Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
+    /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
@@ -295,7 +303,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     rate: nil,
                     rate: nil,
                     eventType: .capillaryGlucose,
                     eventType: .capillaryGlucose,
                     createdAt: result.date,
                     createdAt: result.date,
-                    enteredBy: "Trio",
+                    enteredBy: CarbsEntry.manual,
                     bolus: nil,
                     bolus: nil,
                     insulin: nil,
                     insulin: nil,
                     notes: "Trio User",
                     notes: "Trio User",
@@ -326,6 +334,158 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
         return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
     }
     }
 
 
+    // Fetch glucose that is not uploaded to Nightscout yet
+    /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
+    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.glucoseNotYetUploadedToHealth,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.map { result in
+                BloodGlucose(
+                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    sgv: Int(result.glucose),
+                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
+                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+                    dateString: result.date ?? Date(),
+                    unfiltered: Decimal(result.glucose),
+                    filtered: Decimal(result.glucose),
+                    noise: nil,
+                    glucose: Int(result.glucose)
+                )
+            }
+        }
+    }
+
+    // Fetch manual glucose that is not uploaded to Nightscout yet
+    /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
+    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.map { result in
+                BloodGlucose(
+                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    sgv: Int(result.glucose),
+                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
+                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+                    dateString: result.date ?? Date(),
+                    unfiltered: Decimal(result.glucose),
+                    filtered: Decimal(result.glucose),
+                    noise: nil,
+                    glucose: Int(result.glucose)
+                )
+            }
+        }
+    }
+
+    // Fetch glucose that is not uploaded to Tidepool yet
+    /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
+    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.map { result in
+                BloodGlucose(
+                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    sgv: Int(result.glucose),
+                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
+                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+                    dateString: result.date ?? Date(),
+                    unfiltered: Decimal(result.glucose),
+                    filtered: Decimal(result.glucose),
+                    noise: nil,
+                    glucose: Int(result.glucose)
+                )
+            }
+            .map { $0.convertStoredGlucoseSample(isManualGlucose: false) }
+        }
+    }
+
+    // Fetch manual glucose that is not uploaded to Tidepool yet
+    /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
+    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
+        return await coredataContext.perform {
+            return fetchedResults.map { result in
+                BloodGlucose(
+                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    sgv: Int(result.glucose),
+                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
+                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+                    dateString: result.date ?? Date(),
+                    unfiltered: Decimal(result.glucose),
+                    filtered: Decimal(result.glucose),
+                    noise: nil,
+                    glucose: Int(result.glucose)
+                )
+            }.map { $0.convertStoredGlucoseSample(isManualGlucose: true) }
+        }
+    }
+
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteGlucose"
+
+        await taskContext.perform {
+            do {
+                let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
+
+                guard let glucoseToDelete = result else {
+                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
+                    return
+                }
+
+                taskContext.delete(glucoseToDelete)
+
+                guard taskContext.hasChanges else { return }
+                try taskContext.save()
+                debugPrint("\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
+            } catch {
+                debugPrint(
+                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
+                )
+            }
+        }
+    }
+
     var alarm: GlucoseAlarm? {
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         coredataContext.performAndWait {
         coredataContext.performAndWait {

+ 109 - 0
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -15,6 +15,8 @@ protocol PumpHistoryStorage {
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func recent() -> [PumpHistoryEvent]
     func recent() -> [PumpHistoryEvent]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
     func deleteInsulin(at date: Date)
     func deleteInsulin(at date: Date)
 }
 }
 
 
@@ -80,6 +82,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                                         existingEvent.bolus?.amount = amount as NSDecimalNumber
                                         existingEvent.bolus?.amount = amount as NSDecimalNumber
                                         existingEvent.bolus?.isSMB = dose.automatic ?? true
                                         existingEvent.bolus?.isSMB = dose.automatic ?? true
                                         existingEvent.isUploadedToNS = false
                                         existingEvent.isUploadedToNS = false
+                                        existingEvent.isUploadedToHealth = false
+                                        existingEvent.isUploadedToTidepool = false
 
 
                                         print("Updated existing event with smaller value: \(amount)")
                                         print("Updated existing event with smaller value: \(amount)")
                                     }
                                     }
@@ -93,6 +97,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.type = PumpEvent.bolus.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                         let newBolusEntry = BolusStored(context: self.context)
                         let newBolusEntry = BolusStored(context: self.context)
                         newBolusEntry.pumpEvent = newPumpEvent
                         newBolusEntry.pumpEvent = newPumpEvent
@@ -122,6 +128,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = date
                         newPumpEvent.timestamp = date
                         newPumpEvent.type = PumpEvent.tempBasal.rawValue
                         newPumpEvent.type = PumpEvent.tempBasal.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                         let newTempBasal = TempBasalStored(context: self.context)
                         let newTempBasal = TempBasalStored(context: self.context)
                         newTempBasal.pumpEvent = newPumpEvent
                         newTempBasal.pumpEvent = newPumpEvent
@@ -140,6 +148,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
                         newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .resume:
                     case .resume:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -152,6 +162,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpResume.rawValue
                         newPumpEvent.type = PumpEvent.pumpResume.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .rewind:
                     case .rewind:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -164,6 +176,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.rewind.rawValue
                         newPumpEvent.type = PumpEvent.rewind.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .prime:
                     case .prime:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -176,6 +190,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.prime.rawValue
                         newPumpEvent.type = PumpEvent.prime.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
 
 
                     case .alarm:
                     case .alarm:
                         guard existingEvents.isEmpty else {
                         guard existingEvents.isEmpty else {
@@ -188,6 +204,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.timestamp = event.date
                         newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
                         newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
                         newPumpEvent.isUploadedToNS = false
                         newPumpEvent.isUploadedToNS = false
+                        newPumpEvent.isUploadedToHealth = false
+                        newPumpEvent.isUploadedToTidepool = false
                         newPumpEvent.note = event.title
                         newPumpEvent.note = event.title
 
 
                     default:
                     default:
@@ -217,6 +235,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             newPumpEvent.timestamp = timestamp
             newPumpEvent.timestamp = timestamp
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.isUploadedToNS = false
             newPumpEvent.isUploadedToNS = false
+            newPumpEvent.isUploadedToHealth = false
+            newPumpEvent.isUploadedToTidepool = false
 
 
             // create bolus entry and specify relationship to pump event
             // create bolus entry and specify relationship to pump event
             let newBolusEntry = BolusStored(context: self.context)
             let newBolusEntry = BolusStored(context: self.context)
@@ -423,4 +443,93 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             }.compactMap { $0 }
             }.compactMap { $0 }
         }
         }
     }
     }
+
+    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+        return await context.perform {
+            fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEvent.bolus.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .bolus,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.bolus?.amount as Decimal?
+                    )
+                case PumpEvent.tempBasal.rawValue:
+                    if let id = event.id, let timestamp = event.timestamp, let tempBasal = event.tempBasal,
+                       let tempBasalRate = tempBasal.rate
+                    {
+                        return PumpHistoryEvent(
+                            id: id,
+                            type: .tempBasal,
+                            timestamp: timestamp,
+                            amount: tempBasalRate as Decimal,
+                            duration: Int(tempBasal.duration)
+                        )
+                    } else {
+                        return nil
+                    }
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
+        }
+    }
+
+    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+        return await context.perform {
+            fetchedPumpEvents.map { event in
+                switch event.type {
+                case PumpEvent.bolus.rawValue:
+                    return PumpHistoryEvent(
+                        id: event.id ?? UUID().uuidString,
+                        type: .bolus,
+                        timestamp: event.timestamp ?? Date(),
+                        amount: event.bolus?.amount as Decimal?,
+                        isSMB: event.bolus?.isSMB ?? true,
+                        isExternal: event.bolus?.isExternal ?? false
+                    )
+                case PumpEvent.tempBasal.rawValue:
+                    if let id = event.id, let timestamp = event.timestamp, let tempBasal = event.tempBasal,
+                       let tempBasalRate = tempBasal.rate
+                    {
+                        return PumpHistoryEvent(
+                            id: id,
+                            type: .tempBasal,
+                            timestamp: timestamp,
+                            amount: tempBasalRate as Decimal,
+                            duration: Int(tempBasal.duration)
+                        )
+                    } else {
+                        return nil
+                    }
+
+                default:
+                    return nil
+                }
+            }.compactMap { $0 }
+        }
+    }
 }
 }

+ 3 - 2
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -157,12 +157,13 @@ extension BloodGlucose: SavitzkyGolaySmoothable {
 }
 }
 
 
 extension BloodGlucose {
 extension BloodGlucose {
-    func convertStoredGlucoseSample(device: HKDevice?) -> StoredGlucoseSample {
+    func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
         StoredGlucoseSample(
         StoredGlucoseSample(
             syncIdentifier: id,
             syncIdentifier: id,
             startDate: dateString.date,
             startDate: dateString.date,
             quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
             quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
-            device: device
+            wasUserEntered: isManualGlucose,
+            device: HKDevice.local()
         )
         )
     }
     }
 }
 }

+ 1 - 1
FreeAPS/Sources/Models/CarbsEntry.swift

@@ -49,7 +49,7 @@ extension CarbsEntry {
             grams: Double(carbs),
             grams: Double(carbs),
             startDate: createdAt,
             startDate: createdAt,
             uuid: UUID(uuidString: id!),
             uuid: UUID(uuidString: id!),
-            provenanceIdentifier: enteredBy ?? "",
+            provenanceIdentifier: enteredBy ?? "Trio",
             syncIdentifier: id,
             syncIdentifier: id,
             syncVersion: nil,
             syncVersion: nil,
             userCreatedDate: nil,
             userCreatedDate: nil,

+ 7 - 2
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -1,5 +1,6 @@
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import HealthKit
 import SwiftUI
 import SwiftUI
 
 
 enum DataTable {
 enum DataTable {
@@ -218,6 +219,10 @@ enum DataTable {
 }
 }
 
 
 protocol DataTableProvider: Provider {
 protocol DataTableProvider: Provider {
-    func deleteCarbsFromNightscout(withID id: String) async
-    func deleteInsulin(with treatmentObjectID: NSManagedObjectID) async
+    func deleteCarbsFromNightscout(withID id: String)
+    func deleteInsulinFromNightscout(withID id: String)
+    func deleteManualGlucoseFromNightscout(withID id: String)
+    func deleteGlucoseFromHealth(withSyncID id: String)
+    func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType)
+    func deleteInsulinFromHealth(withSyncID id: String)
 }
 }

+ 36 - 34
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -1,5 +1,6 @@
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import HealthKit
 
 
 extension DataTable {
 extension DataTable {
     final class Provider: BaseProvider, DataTableProvider {
     final class Provider: BaseProvider, DataTableProvider {
@@ -14,52 +15,53 @@ extension DataTable {
         }
         }
 
 
         func deleteCarbsFromNightscout(withID id: String) {
         func deleteCarbsFromNightscout(withID id: String) {
-            Task {
-                await nightscoutManager.deleteCarbs(withID: id)
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.nightscoutManager.deleteCarbs(withID: id)
             }
             }
         }
         }
 
 
-        func deleteInsulin(with treatmentObjectID: NSManagedObjectID) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-
-            await taskContext.perform {
-                do {
-                    guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
-                    else {
-                        debug(.default, "Could not cast the object to PumpEventStored")
-                        return
-                    }
-
-                    // Delete Insulin from Nightscout
-                    if let id = treatmentToDelete.id {
-                        self.deleteInsulinFromNightscout(withID: id)
-                    }
-
-                    // TODO: - Rewrite healthkit implementation
-
-//                    let id = treatmentToDelete.id
-//                    self.healthkitManager.deleteInsulin(syncID: id)
+        func deleteInsulinFromNightscout(withID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.nightscoutManager.deleteInsulin(withID: id)
+            }
+        }
 
 
-                    taskContext.delete(treatmentToDelete)
-                    try taskContext.save()
+        func deleteInsulinFromHealth(withSyncID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.healthkitManager.deleteInsulin(syncID: id)
+            }
+        }
 
 
-                    debug(.default, "Successfully deleted the treatment object.")
-                } catch {
-                    debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
-                }
+        func deleteManualGlucoseFromNightscout(withID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.nightscoutManager.deleteManualGlucose(withID: id)
             }
             }
         }
         }
 
 
-        func deleteInsulinFromNightscout(withID id: String) {
-            Task {
-                await nightscoutManager.deleteInsulin(withID: id)
+        func deleteGlucoseFromHealth(withSyncID id: String) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.healthkitManager.deleteGlucose(syncID: id)
             }
             }
         }
         }
 
 
-        func deleteManualGlucose(withID id: String) {
-            Task {
-                await nightscoutManager.deleteManualGlucose(withID: id)
+        func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType) {
+            Task.detached { [weak self] in
+                guard let self = self else { return }
+                await self.healthkitManager.deleteMealData(byID: id, sampleType: sampleType)
             }
             }
         }
         }
+
+        func deleteInsulinFromTidepool(withSyncId id: String, amount: Decimal, at: Date) {
+            tidepoolManager.deleteInsulin(withSyncId: id, amount: amount, at: at)
+        }
+
+        func deleteCarbsFromTidepool(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String) {
+            tidepoolManager.deleteCarbs(withSyncId: id, carbs: carbs, at: at, enteredBy: enteredBy)
+        }
     }
     }
 }
 }

+ 120 - 45
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -1,4 +1,5 @@
 import CoreData
 import CoreData
+import HealthKit
 import SwiftUI
 import SwiftUI
 
 
 extension DataTable {
 extension DataTable {
@@ -37,19 +38,26 @@ extension DataTable {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
         }
 
 
-        // Carb and FPU deletion from history
-        /// marked as MainActor to be able to publish changes from the background
-        /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        @MainActor func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        // Glucose deletion from history and from remote services
+        /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
             Task {
                 await deleteGlucose(treatmentObjectID)
                 await deleteGlucose(treatmentObjectID)
             }
             }
         }
         }
 
 
         func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
         func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+            // Delete from Apple Health/Tidepool
+            await deleteGlucoseFromServices(treatmentObjectID)
+
+            // Delete from Core Data
+            await glucoseStorage.deleteGlucose(treatmentObjectID)
+        }
+
+        func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
             taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteGlucose"
+            taskContext.transactionAuthor = "deleteGlucoseFromServices"
 
 
             await taskContext.perform {
             await taskContext.perform {
                 do {
                 do {
@@ -60,41 +68,55 @@ extension DataTable {
                         return
                         return
                     }
                     }
 
 
-                    // Delete Manual Glucose from Nightscout
-                    if glucoseToDelete.isManual == true {
-                        if let id = glucoseToDelete.id?.uuidString {
-                            self.provider.deleteManualGlucose(withID: id)
-                        }
+                    // Delete from Nightscout
+                    if let id = glucoseToDelete.id?.uuidString {
+                        self.provider.deleteManualGlucoseFromNightscout(withID: id)
                     }
                     }
 
 
-                    taskContext.delete(glucoseToDelete)
+                    // Delete from Apple Health
+                    if let id = glucoseToDelete.id?.uuidString {
+                        self.provider.deleteGlucoseFromHealth(withSyncID: id)
+                    }
 
 
-                    guard taskContext.hasChanges else { return }
-                    try taskContext.save()
-                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
+                    debugPrint(
+                        "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
+                    )
                 } catch {
                 } catch {
                     debugPrint(
                     debugPrint(
-                        "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
+                        "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
                     )
                     )
                 }
                 }
             }
             }
         }
         }
 
 
         // Carb and FPU deletion from history
         // Carb and FPU deletion from history
-        /// marked as MainActor to be able to publish changes from the background
-        /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        @MainActor func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
             Task {
                 await deleteCarbs(treatmentObjectID)
                 await deleteCarbs(treatmentObjectID)
-                carbEntryDeleted = true
-                waitForSuggestion = true
+
+                await MainActor.run {
+                    carbEntryDeleted = true
+                    waitForSuggestion = true
+                }
             }
             }
         }
         }
 
 
         func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
         func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+            // Delete from Apple Health/Tidepool
+            await deleteCarbsFromServices(treatmentObjectID)
+
+            // Delete from Core Data
+            await carbsStorage.deleteCarbs(treatmentObjectID)
+
+            // Perform a determine basal sync to update cob
+            await apsManager.determineBasalSync()
+        }
+
+        func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
             taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteCarbs"
+            taskContext.transactionAuthor = "deleteCarbsFromServices"
 
 
             var carbEntry: CarbEntryStored?
             var carbEntry: CarbEntryStored?
 
 
@@ -103,46 +125,66 @@ extension DataTable {
                 do {
                 do {
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     guard let carbEntry = carbEntry else {
                     guard let carbEntry = carbEntry else {
-                        debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
+                        debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
                         return
                         return
                     }
                     }
 
 
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
-                        // Delete FPUs from Nightscout
+                        // Delete Fat and Protein entries from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
+
+                        // Delete Fat and Protein entries from Apple Health
+                        let healthObjectsToDelete: [HKSampleType?] = [
+                            AppleHealthConfig.healthFatObject,
+                            AppleHealthConfig.healthProteinObject
+                        ]
+
+                        for sampleType in healthObjectsToDelete {
+                            if let validSampleType = sampleType {
+                                self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
+                            }
+                        }
                     } else {
                     } else {
                         // Delete carbs from Nightscout
                         // Delete carbs from Nightscout
-                        if let id = carbEntry.id?.uuidString {
-                            self.provider.deleteCarbsFromNightscout(withID: id)
+                        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)
+                            }
+
+                            self.provider.deleteCarbsFromTidepool(
+                                withSyncId: id,
+                                carbs: Decimal(carbEntry.carbs),
+                                at: entryDate,
+                                enteredBy: CarbsEntry.manual
+                            )
                         }
                         }
                     }
                     }
 
 
                 } catch {
                 } catch {
                     debugPrint(
                     debugPrint(
-                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from Nightscout: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
                     )
                     )
                 }
                 }
             }
             }
-
-            // Delete carbs from Core Data
-            await carbsStorage.deleteCarbs(treatmentObjectID)
-
-            // Perform a determine basal sync to update cob
-            await apsManager.determineBasalSync()
         }
         }
 
 
         // Insulin deletion from history
         // Insulin deletion from history
-        /// marked as MainActor to be able to publish changes from the background
-        /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        @MainActor func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
             Task {
-                await deleteInsulin(treatmentObjectID)
-                insulinEntryDeleted = true
-                waitForSuggestion = true
+                await invokeInsulinDeletion(treatmentObjectID)
+
+                await MainActor.run {
+                    insulinEntryDeleted = true
+                    waitForSuggestion = true
+                }
             }
             }
         }
         }
 
 
-        func deleteInsulin(_ treatmentObjectID: NSManagedObjectID) async {
+        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
             do {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 let authenticated = try await unlockmanager.unlock()
 
 
@@ -151,14 +193,14 @@ extension DataTable {
                     return
                     return
                 }
                 }
 
 
-                async let deleteNSManagedObjectTask: () = CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
-                async let deleteInsulinFromNightScoutTask: () = provider.deleteInsulin(with: treatmentObjectID)
-                async let determineBasalTask: () = apsManager.determineBasalSync()
+                // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
+                await deleteInsulinFromServices(with: treatmentObjectID)
 
 
-                await deleteNSManagedObjectTask
-                await deleteInsulinFromNightScoutTask
-                await determineBasalTask
+                // Delete from Core Data
+                await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
 
+                // Perform a determine basal sync to update iob
+                await apsManager.determineBasalSync()
             } catch {
             } catch {
                 debugPrint(
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
@@ -166,6 +208,37 @@ extension DataTable {
             }
             }
         }
         }
 
 
+        func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
+            let taskContext = CoreDataStack.shared.newTaskContext()
+            taskContext.name = "deleteContext"
+            taskContext.transactionAuthor = "deleteInsulinFromServices"
+
+            await taskContext.perform {
+                do {
+                    guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
+                    else {
+                        debug(.default, "Could not cast the object to PumpEventStored")
+                        return
+                    }
+
+                    if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
+                       let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
+                    {
+                        self.provider.deleteInsulinFromNightscout(withID: id)
+                        self.provider.deleteInsulinFromHealth(withSyncID: id)
+                        self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
+                    }
+
+                    taskContext.delete(treatmentToDelete)
+                    try taskContext.save()
+
+                    debug(.default, "Successfully deleted the treatment object.")
+                } catch {
+                    debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
+                }
+            }
+        }
+
         func addManualGlucose() {
         func addManualGlucose() {
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let glucoseAsInt = Int(glucose)
             let glucoseAsInt = Int(glucose)
@@ -178,6 +251,8 @@ extension DataTable {
                 newItem.glucose = Int16(glucoseAsInt)
                 newItem.glucose = Int16(glucoseAsInt)
                 newItem.isManual = true
                 newItem.isManual = true
                 newItem.isUploadedToNS = false
                 newItem.isUploadedToNS = false
+                newItem.isUploadedToHealth = false
+                newItem.isUploadedToTidepool = false
 
 
                 do {
                 do {
                     guard self.coredataContext.hasChanges else { return }
                     guard self.coredataContext.hasChanges else { return }

Разница между файлами не показана из-за своего большого размера
+ 18 - 18
FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift


+ 16 - 14
FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift

@@ -14,7 +14,7 @@ extension AppleHealthKit {
 
 
             useAppleHealth = settingsManager.settings.useAppleHealth
             useAppleHealth = settingsManager.settings.useAppleHealth
 
 
-            needShowInformationTextForSetPermissions = healthKitManager.areAllowAllPermissions
+            needShowInformationTextForSetPermissions = healthKitManager.hasGrantedFullWritePermissions
 
 
             subscribeSetting(\.useAppleHealth, on: $useAppleHealth) {
             subscribeSetting(\.useAppleHealth, on: $useAppleHealth) {
                 useAppleHealth = $0
                 useAppleHealth = $0
@@ -26,20 +26,22 @@ extension AppleHealthKit {
                     return
                     return
                 }
                 }
 
 
-                self.healthKitManager.requestPermission { ok, error in
-                    DispatchQueue.main.async {
-                        self.needShowInformationTextForSetPermissions = !self.healthKitManager.checkAvailabilitySaveBG()
+                Task {
+                    do {
+                        let permissionGranted = try await self.healthKitManager.requestPermission()
+
+                        await MainActor.run {
+                            self.needShowInformationTextForSetPermissions = !self.healthKitManager.hasGlucoseWritePermission()
+                        }
+
+                        if permissionGranted {
+                            debug(.service, "Permission granted for HealthKitManager")
+                        } else {
+                            warning(.service, "Permission not granted for HealthKitManager")
+                        }
+                    } catch {
+                        warning(.service, "Error requesting permission for HealthKitManager", error: error)
                     }
                     }
-
-                    guard ok, error == nil else {
-                        warning(.service, "Permission not granted for HealthKitManager", error: error)
-                        return
-                    }
-
-                    debug(.service, "Permission  granted HealthKitManager")
-
-                    self.healthKitManager.createBGObserver()
-                    self.healthKitManager.enableBackgroundDelivery()
                 }
                 }
             }
             }
         }
         }

+ 7 - 2
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -326,12 +326,17 @@ extension NightscoutConfig {
             if glucose.isNotEmpty {
             if glucose.isNotEmpty {
                 await MainActor.run {
                 await MainActor.run {
                     self.backfilling = false
                     self.backfilling = false
-                    self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
-                    self.glucoseStorage.storeGlucose(glucose)
+                }
+
+                glucoseStorage.storeGlucose(glucose)
+
+                Task.detached {
+                    await self.healthKitManager.uploadGlucose()
                 }
                 }
             } else {
             } else {
                 await MainActor.run {
                 await MainActor.run {
                     self.backfilling = false
                     self.backfilling = false
+                    debug(.nightscout, "No glucose values found or fetched to backfill.")
                 }
                 }
             }
             }
         }
         }

+ 14 - 1
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -41,7 +41,20 @@ extension NightscoutConfig {
                     Section(
                     Section(
                         header: Text("Nightscout Integration"),
                         header: Text("Nightscout Integration"),
                         content: {
                         content: {
-                            NavigationLink("Connect", destination: NightscoutConnectView(state: state))
+                            NavigationLink(destination: NightscoutConnectView(state: state), label: {
+                                HStack {
+                                    Text("Connect")
+                                    ZStack {
+                                        if state.isConnectedToNS {
+                                            Image(systemName: "network")
+                                            Image(systemName: "checkmark.circle.fill").foregroundColor(.green).font(.caption2)
+                                                .offset(x: 9, y: 6)
+                                        } else {
+                                            Image(systemName: "network.slash")
+                                        }
+                                    }
+                                }
+                            })
                             NavigationLink("Upload", destination: NightscoutUploadView(state: state))
                             NavigationLink("Upload", destination: NightscoutUploadView(state: state))
                             NavigationLink("Fetch & Remote Control", destination: NightscoutFetchView(state: state))
                             NavigationLink("Fetch & Remote Control", destination: NightscoutFetchView(state: state))
                         }
                         }

+ 1 - 1
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -99,6 +99,6 @@ extension Settings.StateModel: ServiceOnboardingDelegate {
 extension Settings.StateModel: CompletionDelegate {
 extension Settings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
         setupTidepool = false
         setupTidepool = false
-        provider.tidepoolManager.forceUploadData(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
+        provider.tidepoolManager.forceTidepoolDataUpload()
     }
     }
 }
 }

+ 29 - 5
FreeAPS/Sources/Modules/Settings/View/TidepoolStartView.swift

@@ -36,12 +36,36 @@ struct TidepoolStartView: BaseView {
                 content:
                 content:
                 {
                 {
                     VStack {
                     VStack {
-                        Button {
-                            state.setupTidepool.toggle()
+                        if let serviceUIType = state.serviceUIType,
+                           let pluginHost = state.provider.tidepoolManager.getTidepoolPluginHost()
+                        {
+                            if let serviceUI = state.provider.tidepoolManager.getTidepoolServiceUI()
+                            {
+                                Button {
+                                    state.setupTidepool.toggle()
+                                }
+                                label: {
+                                    HStack {
+                                        Text("Connected to Tidepool").font(.title3)
+                                        ZStack {
+                                            Image(systemName: "network")
+                                            Image(systemName: "checkmark.circle.fill")
+                                                .foregroundColor(.green).font(.caption2)
+                                                .offset(x: 9, y: 6)
+                                        }
+                                    }
+                                }
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .buttonStyle(.bordered)
+                            } else {
+                                Button {
+                                    state.setupTidepool.toggle()
+                                }
+                                label: { Text("Connect to Tidepool").font(.title3) }
+                                    .frame(maxWidth: .infinity, alignment: .center)
+                                    .buttonStyle(.bordered)
+                            }
                         }
                         }
-                        label: { Text("Connect to Tidepool").font(.title3) }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .buttonStyle(.bordered)
 
 
                         HStack(alignment: .top) {
                         HStack(alignment: .top) {
                             Text("You can connect Trio to seamlessly upload and manage your diabetes data on Tidepool.")
                             Text("You can connect Trio to seamlessly upload and manage your diabetes data on Tidepool.")

Разница между файлами не показана из-за своего большого размера
+ 500 - 487
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift


+ 11 - 1
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -85,6 +85,16 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+
         registerHandlers()
         registerHandlers()
         setupNotification()
         setupNotification()
     }
     }
@@ -395,7 +405,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
         }
         }
 
 
-        if var fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
+        if let fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
             var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
             var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
             modifiedFetchedEnactedDetermination?
             modifiedFetchedEnactedDetermination?
                 .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)
                 .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)

+ 474 - 245
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -1,4 +1,5 @@
 import Combine
 import Combine
+import CoreData
 import Foundation
 import Foundation
 import HealthKit
 import HealthKit
 import LoopKit
 import LoopKit
@@ -9,13 +10,12 @@ protocol TidepoolManager {
     func addTidepoolService(service: Service)
     func addTidepoolService(service: Service)
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolPluginHost() -> PluginHost?
     func getTidepoolPluginHost() -> PluginHost?
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
-    func deleteInsulin(at date: Date)
-//    func uploadStatus()
-    func uploadGlucose(device: HKDevice?) async
-    func forceUploadData(device: HKDevice?)
-//    func uploadPreferences(_ preferences: Preferences)
-//    func uploadProfileAndSettings(_: Bool)
+    func uploadCarbs() async
+    func deleteCarbs(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String)
+    func uploadInsulin() async
+    func deleteInsulin(withSyncId id: String, amount: Decimal, at: Date)
+    func uploadGlucose() async
+    func forceTidepoolDataUpload()
 }
 }
 
 
 final class BaseTidepoolManager: TidepoolManager, Injectable {
 final class BaseTidepoolManager: TidepoolManager, Injectable {
@@ -25,6 +25,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
+    @Injected() private var apsManager: APSManager!
 
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var tidepoolService: RemoteDataService? {
     private var tidepoolService: RemoteDataService? {
@@ -37,15 +38,39 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
         }
     }
     }
 
 
+    private var backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
+
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         loadTidepoolManager()
         loadTidepoolManager()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+
+        registerHandlers()
+
         subscribe()
         subscribe()
     }
     }
 
 
-    /// load the Tidepool Remote Data Service if available
+    /// Loads the Tidepool service from saved state
     fileprivate func loadTidepoolManager() {
     fileprivate func loadTidepoolManager() {
         if let rawTidepoolManager = rawTidepoolManager {
         if let rawTidepoolManager = rawTidepoolManager {
             tidepoolService = tidepoolServiceFromRaw(rawTidepoolManager)
             tidepoolService = tidepoolServiceFromRaw(rawTidepoolManager)
@@ -54,39 +79,62 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
         }
     }
     }
 
 
-    /// allows access to tidepoolService as a simple ServiceUI
+    /// Returns the Tidepool service UI if available
     func getTidepoolServiceUI() -> ServiceUI? {
     func getTidepoolServiceUI() -> ServiceUI? {
-        if let tidepoolService = self.tidepoolService {
-            return tidepoolService as! any ServiceUI as ServiceUI
-        } else {
-            return nil
-        }
+        tidepoolService as? ServiceUI
     }
     }
 
 
-    /// get the pluginHost of Tidepool
+    /// Returns the Tidepool plugin host
     func getTidepoolPluginHost() -> PluginHost? {
     func getTidepoolPluginHost() -> PluginHost? {
         self as PluginHost
         self as PluginHost
     }
     }
 
 
+    /// Adds a Tidepool service
     func addTidepoolService(service: Service) {
     func addTidepoolService(service: Service) {
-        tidepoolService = service as! any RemoteDataService as RemoteDataService
+        tidepoolService = service as? RemoteDataService
     }
     }
 
 
-    /// load the Tidepool Remote Data Service from raw storage
+    /// Loads the Tidepool service from raw stored data
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
-        else {
-            return nil
-        }
+        else { return nil }
+
         if let service = serviceType.init(rawState: rawState) {
         if let service = serviceType.init(rawState: rawState) {
-            return service as! any RemoteDataService as RemoteDataService
-        } else { return nil }
+            return service as? RemoteDataService
+        }
+        return nil
+    }
+
+    /// Registers handlers for Core Data changes
+    private func registerHandlers() {
+        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadInsulin()
+            }
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadCarbs()
+            }
+        }.store(in: &subscriptions)
+
+        // This works only for manual Glucose
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadGlucose()
+            }
+        }.store(in: &subscriptions)
     }
     }
 
 
     private func subscribe() {
     private func subscribe() {
-        broadcaster.register(PumpHistoryObserver.self, observer: self)
-        broadcaster.register(CarbsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
         broadcaster.register(TempTargetsObserver.self, observer: self)
     }
     }
 
 
@@ -94,9 +142,63 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         nil
         nil
     }
     }
 
 
-    func uploadCarbs() {
-        let carbs: [CarbsEntry] = carbsStorage.recent()
+    /// Forces a full data upload to Tidepool
+    func forceTidepoolDataUpload() {
+        Task {
+            await uploadInsulin()
+            await uploadCarbs()
+            await uploadGlucose()
+        }
+    }
+}
+
+extension BaseTidepoolManager: TempTargetsObserver {
+    func tempTargetsDidUpdate(_: [TempTarget]) {}
+}
+
+extension BaseTidepoolManager: ServiceDelegate {
+    var hostIdentifier: String {
+        // TODO: shouldn't this rather be `org.nightscout.Trio` ?
+        "com.loopkit.Loop" // To check
+    }
+
+    var hostVersion: String {
+        var semanticVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
+
+        while semanticVersion.split(separator: ".").count < 3 {
+            semanticVersion += ".0"
+        }
+
+        semanticVersion += "+\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String)"
+
+        return semanticVersion
+    }
+
+    func issueAlert(_: LoopKit.Alert) {}
+
+    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
+
+    func enactRemoteOverride(name _: String, durationTime _: TimeInterval?, remoteAddress _: String) async throws {}
+
+    func cancelRemoteOverride() async throws {}
 
 
+    func deliverRemoteCarbs(
+        amountInGrams _: Double,
+        absorptionTime _: TimeInterval?,
+        foodType _: String?,
+        startDate _: Date?
+    ) async throws {}
+
+    func deliverRemoteBolus(amountInUnits _: Double) async throws {}
+}
+
+/// Carb Upload and Deletion Functionality
+extension BaseTidepoolManager {
+    func uploadCarbs() async {
+        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
+    }
+
+    func uploadCarbs(_ carbs: [CarbsEntry]) {
         guard !carbs.isEmpty, let tidepoolService = self.tidepoolService else { return }
         guard !carbs.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
 
         processQueue.async {
         processQueue.async {
@@ -108,62 +210,223 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                 tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                     switch result {
                     switch result {
                     case let .failure(error):
                     case let .failure(error):
-                        debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                        debug(.nightscout, "Error synchronizing carbs data with Tidepool: \(String(describing: error))")
                     case .success:
                     case .success:
-                        debug(.nightscout, "Success synchronizing carbs data:")
+                        debug(.nightscout, "Success synchronizing carbs data. Upload to Tidepool complete.")
+                        // After successful upload, update the isUploadedToTidepool flag in Core Data
+                        Task {
+                            await self.updateCarbsAsUploaded(carbs)
+                        }
                     }
                     }
                 }
                 }
             }
             }
         }
         }
     }
     }
 
 
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID _: String) {
-        guard let tidepoolService = self.tidepoolService else { return }
+    private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
+        await backgroundContext.perform {
+            let ids = carbs.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
 
-        processQueue.async {
-            var carbsToDelete: [CarbsEntry] = []
-            let allValues = self.storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToTidepool = true
+                }
 
 
-            if let isFPU = isFPU, isFPU {
-                guard let fpuID = fpuID else { return }
-                carbsToDelete = allValues.filter { $0.fpuID == fpuID }.removeDublicates()
-            } else {
-                carbsToDelete = allValues.filter { $0.createdAt == date }.removeDublicates()
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
+                )
             }
             }
+        }
+    }
 
 
-            let syncCarb = carbsToDelete.map { d in
-                d.convertSyncCarb(operation: .delete)
-            }
+    func deleteCarbs(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String) {
+        guard let tidepoolService = self.tidepoolService else { return }
+
+        processQueue.async {
+            let syncCarb: [SyncCarbObject] = [SyncCarbObject(
+                absorptionTime: nil,
+                createdByCurrentApp: true,
+                foodType: nil,
+                grams: Double(carbs),
+                startDate: at,
+                uuid: id,
+                provenanceIdentifier: enteredBy,
+                syncIdentifier: id.uuidString,
+                syncVersion: nil,
+                userCreatedDate: nil,
+                userUpdatedDate: nil,
+                userDeletedDate: nil,
+                operation: LoopKit.Operation.delete,
+                addedDate: nil,
+                supercededDate: nil
+            )]
 
 
             tidepoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
             tidepoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
                 switch result {
                 switch result {
                 case let .failure(error):
                 case let .failure(error):
-                    debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                    debug(.nightscout, "Error synchronizing carbs data with Tidepool: \(String(describing: error))")
                 case .success:
                 case .success:
-                    debug(.nightscout, "Success synchronizing carbs data:")
+                    debug(.nightscout, "Success synchronizing carbs data. Upload to Tidepool complete.")
                 }
                 }
             }
             }
         }
         }
     }
     }
+}
 
 
-    func deleteInsulin(at d: Date) {
-        let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
+/// Insulin Upload and Deletion Functionality
+extension BaseTidepoolManager {
+    func uploadInsulin() async {
+        await uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
+    }
 
 
-        guard !allValues.isEmpty, let tidepoolService = self.tidepoolService else { return }
+    func uploadDose(_ events: [PumpHistoryEvent]) async {
+        guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
+
+        // Fetch all temp basal entries from Core Data for the last 24 hours
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: backgroundContext,
+            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                NSPredicate.pumpHistoryLast24h,
+                NSPredicate(format: "tempBasal != nil")
+            ]),
+            key: "timestamp",
+            ascending: true,
+            batchSize: 50
+        )
+
+        // Ensure that the processing happens within the background context for thread safety
+        await backgroundContext.perform {
+            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
+
+            let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
+                var result = result
+                switch event.type {
+                case .tempBasal:
+                    result
+                        .append(contentsOf: self.processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries))
+                case .bolus:
+                    let bolusDoseEntry = DoseEntry(
+                        type: .bolus,
+                        startDate: event.timestamp,
+                        endDate: event.timestamp,
+                        value: Double(event.amount!),
+                        unit: .units,
+                        deliveredUnits: nil,
+                        syncIdentifier: event.id,
+                        scheduledBasalRate: nil,
+                        insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                        automatic: event.isSMB ?? true,
+                        manuallyEntered: event.isExternal ?? false
+                    )
+                    result.append(bolusDoseEntry)
+                default:
+                    break
+                }
+                return result
+            }
+
+            debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
+
+            let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
+                if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
+                    let dose: DoseEntry? = switch pumpEventType {
+                    case .suspend:
+                        DoseEntry(suspendDate: event.timestamp, automatic: true)
+                    case .resume:
+                        DoseEntry(resumeDate: event.timestamp, automatic: true)
+                    default:
+                        nil
+                    }
+
+                    return PersistedPumpEvent(
+                        date: event.timestamp,
+                        persistedDate: event.timestamp,
+                        dose: dose,
+                        isUploaded: true,
+                        objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
+                        raw: event.id.data(using: .utf8),
+                        title: event.note,
+                        type: pumpEventType
+                    )
+                } else {
+                    return nil
+                }
+            }
+
+            self.processQueue.async {
+                tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
+                    switch result {
+                    case let .failure(error):
+                        debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
+                    case .success:
+                        debug(.nightscout, "Success synchronizing dose data. Upload to Tidepool complete.")
+                        Task {
+                            let insulinEvents = events.filter {
+                                $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+                            }
+                            await self.updateInsulinAsUploaded(insulinEvents)
+                        }
+                    }
+                }
 
 
-        var doseDataToDelete: [DoseEntry] = []
+                tidepoolService.uploadPumpEventData(pumpEvents) { result in
+                    switch result {
+                    case let .failure(error):
+                        debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
+                    case .success:
+                        debug(.nightscout, "Success synchronizing pump events data. Upload to Tidepool complete.")
+                        Task {
+                            let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
+                            let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
 
 
-        guard let entry = allValues.first(where: { $0.timestamp == d }) else {
-            return
+                            await self.updateInsulinAsUploaded(pumpEvents)
+                        }
+                    }
+                }
+            }
         }
         }
-        doseDataToDelete
-            .append(DoseEntry(
-                type: .bolus,
-                startDate: entry.timestamp,
-                value: Double(entry.amount!),
-                unit: .units,
-                syncIdentifier: entry.id
-            ))
+    }
+
+    private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
+        await backgroundContext.perform {
+            let ids = insulin.map(\.id) as NSArray
+            let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToTidepool = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    func deleteInsulin(withSyncId id: String, amount: Decimal, at: Date) {
+        guard let tidepoolService = self.tidepoolService else { return }
+
+        // must be an array here, because `tidepoolService.uploadDoseData` expects a `deleted` array
+        let doseDataToDelete: [DoseEntry] = [DoseEntry(
+            type: .bolus,
+            startDate: at,
+            value: Double(amount),
+            unit: .units,
+            syncIdentifier: id
+        )]
 
 
         processQueue.async {
         processQueue.async {
             tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
             tidepoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
@@ -171,239 +434,205 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 case let .failure(error):
                 case let .failure(error):
                     debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
                     debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
                 case .success:
                 case .success:
-                    debug(.nightscout, "Success synchronizing Dose delete data:")
+                    debug(.nightscout, "Success synchronizing Dose delete data")
                 }
                 }
             }
             }
         }
         }
     }
     }
+}
 
 
-    func uploadDose() {
-        let events = pumpHistoryStorage.recent()
-        guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
-
-        let eventsBasal = events.filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
-            .sorted { $0.timestamp < $1.timestamp }
-
-        let doseDataBasal: [DoseEntry] = eventsBasal.reduce([]) { result, event in
-            var result = result
-            switch event.type {
-            case .tempBasal:
-                // update the previous tempBasal with endtime = starttime of the last event
-                if let last: DoseEntry = result.popLast() {
-                    let value = max(
-                        0,
-                        Double(event.timestamp.timeIntervalSince1970 - last.startDate.timeIntervalSince1970) / 3600
-                    ) *
-                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
-                    result.append(DoseEntry(
-                        type: .tempBasal,
-                        startDate: last.startDate,
-                        endDate: event.timestamp,
-                        value: value,
-                        unit: last.unit,
-                        deliveredUnits: value,
-                        syncIdentifier: last.syncIdentifier,
-                        // scheduledBasalRate: last.scheduledBasalRate,
-                        insulinType: last.insulinType,
-                        automatic: last.automatic,
-                        manuallyEntered: last.manuallyEntered
-                    ))
+/// Insulin Helper Functions
+extension BaseTidepoolManager {
+    private func processTempBasalEvent(
+        _ event: PumpHistoryEvent,
+        existingTempBasalEntries: [PumpEventStored]
+    ) -> [DoseEntry] {
+        var insulinDoseEvents: [DoseEntry] = []
+
+        backgroundContext.performAndWait {
+            // Loop through the pump history events within the background context
+            guard let duration = event.duration, let amount = event.amount,
+                  let currentBasalRate = self.getCurrentBasalRate()
+            else {
+                return
+            }
+            let value = (Decimal(duration) / 60.0) * amount
+
+            // Find the corresponding temp basal entry in existingTempBasalEntries
+            if let matchingEntryIndex = existingTempBasalEntries.firstIndex(where: { $0.timestamp == event.timestamp }) {
+                // Check for a predecessor (the entry before the matching entry)
+                let predecessorIndex = matchingEntryIndex - 1
+                if predecessorIndex >= 0 {
+                    let predecessorEntry = existingTempBasalEntries[predecessorIndex]
+                    if let predecessorTimestamp = predecessorEntry.timestamp,
+                       let predecessorEntrySyncIdentifier = predecessorEntry.id
+                    {
+                        let predecessorEndDate = predecessorTimestamp
+                            .addingTimeInterval(TimeInterval(
+                                Int(predecessorEntry.tempBasal?.duration ?? 0) *
+                                    60
+                            )) // parse duration to minutes
+
+                        // If the predecessor's end date is later than the current event's start date, adjust it
+                        if predecessorEndDate > event.timestamp {
+                            let adjustedEndDate = event.timestamp
+                            let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
+                            let adjustedDeliveredUnits = (adjustedDuration / 3600) *
+                                Double(truncating: predecessorEntry.tempBasal?.rate ?? 0)
+
+                            // Create updated predecessor dose entry
+                            let updatedPredecessorEntry = DoseEntry(
+                                type: .tempBasal,
+                                startDate: predecessorTimestamp,
+                                endDate: adjustedEndDate,
+                                value: adjustedDeliveredUnits,
+                                unit: .units,
+                                deliveredUnits: adjustedDeliveredUnits,
+                                syncIdentifier: predecessorEntrySyncIdentifier,
+                                insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                                automatic: true,
+                                manuallyEntered: false,
+                                isMutable: false
+                            )
+                            // Add the updated predecessor entry to the result
+                            insulinDoseEvents.append(updatedPredecessorEntry)
+                        }
+                    }
                 }
                 }
-                result.append(DoseEntry(
+
+                // Create a new dose entry for the current event
+                let currentEndDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration)))
+                let newDoseEntry = DoseEntry(
                     type: .tempBasal,
                     type: .tempBasal,
                     startDate: event.timestamp,
                     startDate: event.timestamp,
-                    value: 0.0,
+                    endDate: currentEndDate,
+                    value: Double(value),
                     unit: .units,
                     unit: .units,
+                    deliveredUnits: Double(value),
                     syncIdentifier: event.id,
                     syncIdentifier: event.id,
-                    scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(event.rate!)),
-                    insulinType: nil,
+                    scheduledBasalRate: HKQuantity(
+                        unit: .internationalUnitsPerHour,
+                        doubleValue: Double(currentBasalRate.rate)
+                    ),
+                    insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
                     automatic: true,
                     automatic: true,
                     manuallyEntered: false,
                     manuallyEntered: false,
-                    isMutable: true
-                ))
-            case .tempBasalDuration:
-                if let last: DoseEntry = result.popLast(),
-                   last.type == .tempBasal,
-                   last.startDate == event.timestamp
-                {
-                    let durationMin = event.durationMin ?? 0
-                    // result.append(last)
-                    let value = (Double(durationMin) / 60.0) *
-                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
-                    result.append(DoseEntry(
-                        type: .tempBasal,
-                        startDate: last.startDate,
-                        endDate: Calendar.current.date(byAdding: .minute, value: durationMin, to: last.startDate) ?? last
-                            .startDate,
-                        value: value,
-                        unit: last.unit,
-                        deliveredUnits: value,
-                        syncIdentifier: last.syncIdentifier,
-                        scheduledBasalRate: last.scheduledBasalRate,
-                        insulinType: last.insulinType,
-                        automatic: last.automatic,
-                        manuallyEntered: last.manuallyEntered
-                    ))
-                }
-            default: break
-            }
-            return result
-        }
-
-        let boluses: [DoseEntry] = events.compactMap { event -> DoseEntry? in
-            switch event.type {
-            case .bolus:
-                return DoseEntry(
-                    type: .bolus,
-                    startDate: event.timestamp,
-                    endDate: event.timestamp,
-                    value: Double(event.amount!),
-                    unit: .units,
-                    deliveredUnits: nil,
-                    syncIdentifier: event.id,
-                    scheduledBasalRate: nil,
-                    insulinType: nil,
-                    automatic: true,
-                    manuallyEntered: false
+                    isMutable: false
                 )
                 )
-            default: return nil
+                // Add the new event entry to the result
+                insulinDoseEvents.append(newDoseEntry)
             }
             }
         }
         }
 
 
-        let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
-            if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
-                let dose: DoseEntry? = switch pumpEventType {
-                case .suspend:
-                    DoseEntry(suspendDate: event.timestamp, automatic: true)
-                case .resume:
-                    DoseEntry(resumeDate: event.timestamp, automatic: true)
-                default:
-                    nil
-                }
+        return insulinDoseEvents
+    }
 
 
-                return PersistedPumpEvent(
-                    date: event.timestamp,
-                    persistedDate: event.timestamp,
-                    dose: dose,
-                    isUploaded: true,
-                    objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
-                    raw: event.id.data(using: .utf8),
-                    title: event.note,
-                    type: pumpEventType
-                )
-            } else {
-                return nil
+    private func getCurrentBasalRate() -> BasalProfileEntry? {
+        let now = Date()
+        let calendar = Calendar.current
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "HH:mm:ss"
+        dateFormatter.timeZone = TimeZone.current
+
+        let basalEntries = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+            ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
+            ?? []
+
+        var currentRate: BasalProfileEntry = basalEntries[0]
+
+        for (index, entry) in basalEntries.enumerated() {
+            guard let entryTime = dateFormatter.date(from: entry.start) else {
+                print("Invalid entry start time: \(entry.start)")
+                continue
             }
             }
-        }
 
 
-        processQueue.async {
-            tidepoolService.uploadDoseData(created: doseDataBasal + boluses, deleted: []) { result in
-                switch result {
-                case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
-                case .success:
-                    debug(.nightscout, "Success synchronizing Dose data:")
-                }
+            let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+            let entryStartTime = calendar.date(
+                bySettingHour: entryComponents.hour!,
+                minute: entryComponents.minute!,
+                second: entryComponents.second!,
+                of: now
+            )!
+
+            let entryEndTime: Date
+            if index < basalEntries.count - 1,
+               let nextEntryTime = dateFormatter.date(from: basalEntries[index + 1].start)
+            {
+                let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                entryEndTime = calendar.date(
+                    bySettingHour: nextEntryComponents.hour!,
+                    minute: nextEntryComponents.minute!,
+                    second: nextEntryComponents.second!,
+                    of: now
+                )!
+            } else {
+                entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
             }
             }
 
 
-            tidepoolService.uploadPumpEventData(pumpEvents) { result in
-                switch result {
-                case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
-                case .success:
-                    debug(.nightscout, "Success synchronizing Pump Event data:")
-                }
+            if now >= entryStartTime, now < entryEndTime {
+                currentRate = entry
             }
             }
         }
         }
+
+        return currentRate
     }
     }
+}
 
 
-    func uploadGlucose(device: HKDevice?) async {
-        // TODO: get correct glucose values
-        let glucose: [BloodGlucose] = await glucoseStorage.getGlucoseNotYetUploadedToNightscout()
+/// Glucose Upload Functionality
+extension BaseTidepoolManager {
+    func uploadGlucose() async {
+        uploadGlucose(await glucoseStorage.getGlucoseNotYetUploadedToTidepool())
+        uploadGlucose(
+            await glucoseStorage
+                .getManualGlucoseNotYetUploadedToTidepool()
+        )
+    }
 
 
+    func uploadGlucose(_ glucose: [StoredGlucoseSample]) {
         guard !glucose.isEmpty, let tidepoolService = self.tidepoolService else { return }
         guard !glucose.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
 
-        let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id ?? UUID().uuidString) != nil }
-
-        let chunks = glucoseWithoutCorrectID.chunks(ofCount: tidepoolService.glucoseDataLimit ?? 100)
+        let chunks = glucose.chunks(ofCount: tidepoolService.glucoseDataLimit ?? 100)
 
 
         processQueue.async {
         processQueue.async {
             for chunk in chunks {
             for chunk in chunks {
-                // Link all glucose values with the current device
-                let chunkStoreGlucose = chunk.map { $0.convertStoredGlucoseSample(device: device) }
-
-                tidepoolService.uploadGlucoseData(chunkStoreGlucose) { result in
+                tidepoolService.uploadGlucoseData(chunk) { result in
                     switch result {
                     switch result {
                     case .success:
                     case .success:
-                        debug(.nightscout, "Success synchronizing glucose data:")
+                        debug(.nightscout, "Success synchronizing glucose data")
+
+                        // After successful upload, update the isUploadedToTidepool flag in Core Data
+                        Task {
+                            await self.updateGlucoseAsUploaded(glucose)
+                        }
                     case let .failure(error):
                     case let .failure(error):
                         debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
                         debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
-                        // self.uploadFailed(key)
                     }
                     }
                 }
                 }
             }
             }
         }
         }
     }
     }
 
 
-    /// force to uploads all data in Tidepool Service
-    func forceUploadData(device: HKDevice?) {
-        Task {
-            uploadDose()
-            uploadCarbs()
-            await uploadGlucose(device: device)
-        }
-    }
-}
+    private func updateGlucoseAsUploaded(_ glucose: [StoredGlucoseSample]) async {
+        await backgroundContext.perform {
+            let ids = glucose.map(\.syncIdentifier) as NSArray
+            let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
 
-extension BaseTidepoolManager: PumpHistoryObserver {
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        uploadDose()
-    }
-}
-
-extension BaseTidepoolManager: CarbsObserver {
-    func carbsDidUpdate(_: [CarbsEntry]) {
-        uploadCarbs()
-    }
-}
-
-extension BaseTidepoolManager: TempTargetsObserver {
-    func tempTargetsDidUpdate(_: [TempTarget]) {}
-}
-
-extension BaseTidepoolManager: ServiceDelegate {
-    var hostIdentifier: String {
-        "com.loopkit.Loop" // To check
-    }
-
-    var hostVersion: String {
-        var semanticVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                for result in results {
+                    result.isUploadedToTidepool = true
+                }
 
 
-        while semanticVersion.split(separator: ".").count < 3 {
-            semanticVersion += ".0"
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
+                )
+            }
         }
         }
-
-        semanticVersion += "+\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String)"
-
-        return semanticVersion
     }
     }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func enactRemoteOverride(name _: String, durationTime _: TimeInterval?, remoteAddress _: String) async throws {}
-
-    func cancelRemoteOverride() async throws {}
-
-    func deliverRemoteCarbs(
-        amountInGrams _: Double,
-        absorptionTime _: TimeInterval?,
-        foodType _: String?,
-        startDate _: Date?
-    ) async throws {}
-
-    func deliverRemoteBolus(amountInUnits _: Double) async throws {}
 }
 }
 
 
 extension BaseTidepoolManager: StatefulPluggableDelegate {
 extension BaseTidepoolManager: StatefulPluggableDelegate {

+ 2 - 0
Model/Classes+Properties/CarbEntryStored+CoreDataProperties.swift

@@ -13,6 +13,8 @@ public extension CarbEntryStored {
     @NSManaged var id: UUID?
     @NSManaged var id: UUID?
     @NSManaged var isFPU: Bool
     @NSManaged var isFPU: Bool
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var note: String?
     @NSManaged var protein: Double
     @NSManaged var protein: Double
 }
 }

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

@@ -12,6 +12,8 @@ public extension GlucoseStored {
     @NSManaged var id: UUID?
     @NSManaged var id: UUID?
     @NSManaged var isManual: Bool
     @NSManaged var isManual: Bool
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
 }
 }
 
 
 extension GlucoseStored: Identifiable {}
 extension GlucoseStored: Identifiable {}

+ 2 - 0
Model/Classes+Properties/PumpEventStored+CoreDataProperties.swift

@@ -8,6 +8,8 @@ public extension PumpEventStored {
 
 
     @NSManaged var id: String?
     @NSManaged var id: String?
     @NSManaged var isUploadedToNS: Bool
     @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isUploadedToHealth: Bool
+    @NSManaged var isUploadedToTidepool: Bool
     @NSManaged var note: String?
     @NSManaged var note: String?
     @NSManaged var timestamp: Date?
     @NSManaged var timestamp: Date?
     @NSManaged var type: String?
     @NSManaged var type: String?

+ 19 - 1
Model/Helper/CarbEntryStored+helper.swift

@@ -22,6 +22,24 @@ extension NSPredicate {
         )
         )
     }
     }
 
 
+    static var carbsNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToHealth == %@",
+            date as NSDate,
+            false as NSNumber
+        )
+    }
+
+    static var carbsNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToTidepool == %@",
+            date as NSDate,
+            false as NSNumber
+        )
+    }
+
     static var fpusNotYetUploadedToNightscout: NSPredicate {
     static var fpusNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(
         return NSPredicate(
@@ -71,7 +89,7 @@ extension CarbEntryStored: Encodable {
         try container.encode(formattedDate, forKey: .created_at)
         try container.encode(formattedDate, forKey: .created_at)
 
 
         // TODO: handle this conditionally; pass in the enteredBy string (manual entry or via NS or Apple Health)
         // TODO: handle this conditionally; pass in the enteredBy string (manual entry or via NS or Apple Health)
-        try container.encode("Open-iAPS", forKey: .enteredBy)
+        try container.encode("Trio", forKey: .enteredBy)
 
 
         try container.encode(carbs, forKey: .carbs)
         try container.encode(carbs, forKey: .carbs)
         try container.encode(fat, forKey: .fat)
         try container.encode(fat, forKey: .fat)

+ 30 - 0
Model/Helper/GlucoseStored+helper.swift

@@ -70,6 +70,16 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
         return NSPredicate(format: "date >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
     }
     }
 
 
+    static var glucoseNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
+    }
+
+    static var glucoseNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
+    }
+
     static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
     static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(
         return NSPredicate(
@@ -79,6 +89,26 @@ extension NSPredicate {
             true as NSNumber
             true as NSNumber
         )
         )
     }
     }
+
+    static var manualGlucoseNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToHealth == %@ AND isManual == %@",
+            date as NSDate,
+            false as NSNumber,
+            true as NSNumber
+        )
+    }
+
+    static var manualGlucoseNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND isUploadedToTidepool == %@ AND isManual == %@",
+            date as NSDate,
+            false as NSNumber,
+            true as NSNumber
+        )
+    }
 }
 }
 
 
 extension GlucoseStored: Encodable {
 extension GlucoseStored: Encodable {

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

@@ -82,6 +82,16 @@ extension NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
         return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
     }
     }
+
+    static var pumpEventsNotYetUploadedToHealth: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "timestamp >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
+    }
+
+    static var pumpEventsNotYetUploadedToTidepool: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "timestamp >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
+    }
 }
 }
 
 
 // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
 // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history

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

@@ -13,7 +13,9 @@
         <attribute name="fpuID" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="fpuID" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="isFPU" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isFPU" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
@@ -44,9 +46,11 @@
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="isManual" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isManual" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
-            <fetchIndexElement property="date" type="Binary" order="descending"/>
+            <fetchIndexElement property="date" type="Binary" order="ascending"/>
         </fetchIndex>
         </fetchIndex>
         <fetchIndex name="byIsManual">
         <fetchIndex name="byIsManual">
             <fetchIndexElement property="isManual" type="Binary" order="ascending"/>
             <fetchIndexElement property="isManual" type="Binary" order="ascending"/>
@@ -54,6 +58,9 @@
         <fetchIndex name="byIsUploadedToNS">
         <fetchIndex name="byIsUploadedToNS">
             <fetchIndexElement property="isUploadedToNS" type="Binary" order="ascending"/>
             <fetchIndexElement property="isUploadedToNS" type="Binary" order="ascending"/>
         </fetchIndex>
         </fetchIndex>
+        <fetchIndex name="byIsUploadedToHealth">
+            <fetchIndexElement property="isUploadedToHealth" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     </entity>
     <entity name="LoopStatRecord" representedClassName="LoopStatRecord" syncable="YES">
     <entity name="LoopStatRecord" representedClassName="LoopStatRecord" syncable="YES">
         <attribute name="duration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
         <attribute name="duration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
@@ -161,7 +168,9 @@
     </entity>
     </entity>
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
         <attribute name="id" optional="YES" attributeType="String"/>
         <attribute name="id" optional="YES" attributeType="String"/>
+        <attribute name="isUploadedToHealth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToTidepool" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="note" optional="YES" attributeType="String"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="type" optional="YES" attributeType="String"/>
         <attribute name="type" optional="YES" attributeType="String"/>