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

Adjust TidepoolManager and data handling for Combine Publishers

Deniz Cengiz 1 год назад
Родитель
Сommit
1f3f9a056a

+ 1 - 1
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -266,7 +266,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         Task.detached {
             async let uploadToNS: () = self.nightscoutManager.uploadGlucose()
             async let uploadToHealth: () = self.healthKitManager.uploadGlucose()
-            async let uploadToTidepool: () = self.tidepoolService.uploadGlucose(device: self.cgmManager?.cgmManagerStatus.device)
+            async let uploadToTidepool: () = self.tidepoolService.uploadGlucose()
 
             await uploadToNS
             await uploadToHealth

+ 2 - 1
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -18,6 +18,7 @@ protocol CarbsStorage {
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
+    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
 }
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -450,7 +451,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
-            predicate: NSPredicate.carbsNotYetUploadedToHealth,
+            predicate: NSPredicate.carbsNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )

+ 65 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -2,6 +2,7 @@ import AVFAudio
 import Combine
 import CoreData
 import Foundation
+import LoopKit
 import SwiftDate
 import SwiftUI
 import Swinject
@@ -19,6 +20,8 @@ protocol GlucoseStorage {
     func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
     func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
+    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
+    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
     var alarm: GlucoseAlarm? { get }
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
@@ -393,6 +396,68 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         }
     }
 
+    // Fetch glucose that is not uploaded to Nightscout yet
+    /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
+    func getGlucoseNotYetUploadedToTidepool() 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(device: nil) }
+        }
+    }
+
+    // Fetch manual glucose that is not uploaded to Nightscout yet
+    /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS 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(device: nil) }
+        }
+    }
+
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
         let taskContext = CoreDataStack.shared.newTaskContext()
         taskContext.name = "deleteContext"

+ 1 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -483,7 +483,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
-            predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
+            predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             key: "timestamp",
             ascending: false,
             fetchLimit: 288

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

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

+ 1 - 1
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -73,7 +73,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                 .eraseToAnyPublisher()
 
         registerHandlers()
-        
+
         guard isAvailableOnCurrentDevice,
               AppleHealthConfig.healthBGObject != nil else { return }
 

+ 59 - 38
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -14,11 +14,11 @@ protocol TidepoolManager {
     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(device: HKDevice?) async
-    func forceTidepoolDataUpload(device: HKDevice?)
+    func uploadGlucose() async
+    func forceTidepoolDataUpload()
 }
 
-final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegate, PumpHistoryDelegate {
+final class BaseTidepoolManager: TidepoolManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var pluginManager: PluginManager!
     @Injected() private var glucoseStorage: GlucoseStorage!
@@ -40,19 +40,8 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
 
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
-    func carbsStorageHasUpdatedCarbs(_: BaseCarbsStorage) {
-        Task.detached { [weak self] in
-            guard let self = self else { return }
-            await self.uploadCarbs()
-        }
-    }
-
-    func pumpHistoryHasUpdated(_: BasePumpHistoryStorage) {
-        Task.detached { [weak self] in
-            guard let self = self else { return }
-            await self.uploadInsulin()
-        }
-    }
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
@@ -60,8 +49,13 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
         injectServices(resolver)
         loadTidepoolManager()
 
-        pumpHistoryStorage.delegate = self
-        carbsStorage.delegate = self
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
+        registerHandlers()
 
         subscribe()
     }
@@ -105,6 +99,34 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
         } else { return nil }
     }
 
+    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)
+
+        // TODO: this is currently done in FetchGlucoseManager and forced there inside a background task.
+        // leave it there, or move it here? not sure…
+//        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(device: fetchCgmManager.cgmManager?.cgmManagerStatus.device)
+//            }
+//        }.store(in: &subscriptions)33
+    }
+
     private func subscribe() {
         broadcaster.register(TempTargetsObserver.self, observer: self)
     }
@@ -114,7 +136,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
     }
 
     func uploadCarbs() async {
-        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToHealth())
+        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
     }
 
     func uploadCarbs(_ carbs: [CarbsEntry]) {
@@ -218,7 +240,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
         ).filter { $0.tempBasal != nil }
 
         // remove first fetched existing entry, so new events and old events are off by 1 index, so the same index is always current event in events and the previous event to that one in existing events array.
-        existingTempBasalEntries.removeFirst()
+        if existingTempBasalEntries.isNotEmpty, existingTempBasalEntries.count > 1 {
+            existingTempBasalEntries.removeFirst()
+        }
 
         let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
             var result = result
@@ -242,10 +266,6 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
                             unit: .units,
                             deliveredUnits: lastDeliveredUnits,
                             syncIdentifier: lastEntry.id ?? UUID().uuidString,
-//                            scheduledBasalRate: HKQuantity(
-//                                unit: .internationalUnitsPerHour,
-//                                doubleValue: Double(truncating: lastEntry.tempBasal?.rate ?? 0.0)
-//                            ),
                             insulinType: apsManager.pumpManager?.status.insulinType ?? nil,
                             automatic: true,
                             manuallyEntered: false,
@@ -414,25 +434,26 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
         }
     }
 
-    func uploadGlucose(device: HKDevice?) async {
-        // TODO: get correct glucose values
-        let glucose: [BloodGlucose] = await glucoseStorage.getGlucoseNotYetUploadedToNightscout()
+    func uploadGlucose() async {
+        uploadGlucose(await glucoseStorage.getGlucoseNotYetUploadedToTidepool())
+        uploadGlucose(
+            await glucoseStorage
+                .getManualGlucoseNotYetUploadedToTidepool()
+        )
+    }
 
+    func uploadGlucose(_ glucose: [StoredGlucoseSample]) {
         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 {
             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 {
                     case .success:
                         debug(.nightscout, "Success synchronizing glucose data")
+
                         // After successful upload, update the isUploadedToTidepool flag in Core Data
                         Task {
                             await self.updateGlucoseAsUploaded(glucose)
@@ -445,9 +466,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
         }
     }
 
-    private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
+    private func updateGlucoseAsUploaded(_ glucose: [StoredGlucoseSample]) async {
         await backgroundContext.perform {
-            let ids = glucose.map(\.id) as NSArray
+            let ids = glucose.map(\.syncIdentifier) as NSArray
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
@@ -468,11 +489,11 @@ final class BaseTidepoolManager: TidepoolManager, Injectable, CarbsStoredDelegat
     }
 
     /// force to uploads all data in Tidepool Service
-    func forceTidepoolDataUpload(device: HKDevice?) {
+    func forceTidepoolDataUpload() {
         Task {
             await uploadInsulin()
             await uploadCarbs()
-            await uploadGlucose(device: device)
+            await uploadGlucose()
         }
     }
 }

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

@@ -99,6 +99,16 @@ extension NSPredicate {
             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 {