Pārlūkot izejas kodu

Refactoring and deletion fixes
* Change how fpuID is set and used for carb entries
* Change how fpu entries (carb equivalents) are deleted, making sure no associated carb entry is deleted
* Major cleanup in HealthKitManager.swift
* Fix behaviour for deletion: use fpuID not carb ID
* Ensure carb equivalents and carb entries can still be deleted individually

Deniz Cengiz 1 gadu atpakaļ
vecāks
revīzija
4995754db9

+ 11 - 16
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -165,18 +165,16 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
     public func refreshCGM() {
         debug(.deviceManager, "refreshCGM by pump")
-        // updateGlucoseSource(cgmGlucoseSourceType: settingsManager.settings.cgm, cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier)
-
-        Publishers.CombineLatest3(
+        
+        Publishers.CombineLatest(
             Just(glucoseStorage.syncDate()),
-            healthKitManager.fetch(nil),
             glucoseSource.fetchIfNeeded()
         )
         .eraseToAnyPublisher()
         .receive(on: processQueue)
-        .sink { syncDate, glucoseFromHealth, glucose in
+        .sink { syncDate, glucose in
             debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
-            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
+            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose)
         }
         .store(in: &lifetime)
     }
@@ -210,11 +208,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
         let newGlucose = overcalibrate(entries: glucose)
 
-        let allGlucose = newGlucose + glucoseFromHealth
         var filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
@@ -226,7 +223,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             backGroundFetchBGTaskID = .invalid
         }
 
-        guard allGlucose.isNotEmpty else {
+        guard newGlucose.isNotEmpty else {
             if let backgroundTask = backGroundFetchBGTaskID {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 backGroundFetchBGTaskID = .invalid
@@ -234,7 +231,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             return
         }
 
-        filteredByDate = allGlucose.filter { $0.dateString > syncDate }
+        filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
         guard filtered.isNotEmpty else {
@@ -297,17 +294,15 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             }
             .sink { glucose in
                 debug(.nightscout, "FetchGlucoseManager callback sensor")
-                Publishers.CombineLatest3(
+                Publishers.CombineLatest(
                     Just(glucose),
-                    Just(self.glucoseStorage.syncDate()),
-                    self.healthKitManager.fetch(nil)
+                    Just(self.glucoseStorage.syncDate())
                 )
                 .eraseToAnyPublisher()
-                .sink { newGlucose, syncDate, glucoseFromHealth in
+                .sink { newGlucose, syncDate in
                     self.glucoseStoreAndHeartDecision(
                         syncDate: syncDate,
-                        glucose: newGlucose,
-                        glucoseFromHealth: glucoseFromHealth
+                        glucose: newGlucose
                     )
                 }
                 .store(in: &self.lifetime)

+ 12 - 5
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -46,9 +46,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
 
-        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
 
+        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+
         // TODO: - Should we really use a delegate here? If yes, should we also use this for NS/TP?
 
         delegate?.carbsStorageHasUpdatedCarbs(self)
@@ -121,7 +122,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      */
     private func processFPU(
-        entries _: [CarbsEntry],
+        entries: [CarbsEntry],
         fat: Decimal,
         protein: Decimal,
         createdAt: Date,
@@ -150,7 +151,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var numberOfEquivalents = carbEquivalents / carbEquivalentSize
 
         var useDate = actualDate ?? createdAt
-        let fpuID = UUID().uuidString
+        let fpuID = entries.first?.fpuID ?? UUID().uuidString
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
 
@@ -210,6 +211,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.isUploadedToNS = areFetchedFromRemote ? true : false
             newItem.isUploadedToHealth = false
 
+            if entry.fat != nil, entry.protein != nil, let fpuId = entry.fpuID {
+                newItem.fpuID = UUID(uuidString: fpuId)
+            }
+
             do {
                 guard self.coredataContext.hasChanges else { return }
                 try self.coredataContext.save()
@@ -220,8 +225,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     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
         let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
             guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -130,7 +130,7 @@ extension DataTable {
 
                         // fetch request for all carb entries with the same id
                         let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                        fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
+                        fetchRequest.predicate = NSPredicate(format: "isFPU == true AND fpuID == %@", fpuID as CVarArg)
 
                         // NSBatchDeleteRequest
                         let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

+ 0 - 4
FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift

@@ -36,10 +36,6 @@ extension AppleHealthKit {
 
                         if permissionGranted {
                             debug(.service, "Permission granted for HealthKitManager")
-
-                            self.healthKitManager.createBGObserver()
-                            self.healthKitManager.enableBackgroundDelivery()
-
                         } else {
                             warning(.service, "Permission not granted for HealthKitManager")
                         }

+ 1 - 0
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -336,6 +336,7 @@ extension NightscoutConfig {
             } else {
                 await MainActor.run {
                     self.backfilling = false
+                    debug(.nightscout, "No glucose values found or fetched to backfill.")
                 }
             }
         }

+ 17 - 178
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -6,7 +6,7 @@ import LoopKit
 import LoopKitUI
 import Swinject
 
-protocol HealthKitManager: GlucoseSource {
+protocol HealthKitManager {
     /// Check all needed permissions
     /// Return false if one or more permissions are deny or not choosen
     var areAllowAllPermissions: Bool { get }
@@ -20,10 +20,6 @@ protocol HealthKitManager: GlucoseSource {
     func uploadCarbs() async
     /// Save Insulin to Health store
     func uploadInsulin() async
-    /// Create observer for data passing beetwen Health Store and Trio
-    func createBGObserver()
-    /// Enable background delivering objects from Apple Health to Trio
-    func enableBackgroundDelivery()
     /// Delete glucose with syncID
     func deleteGlucose(syncID: String) async
     /// delete carbs with syncID
@@ -34,9 +30,6 @@ protocol HealthKitManager: GlucoseSource {
 
 public enum AppleHealthConfig {
     // unwraped HKObjects
-    static var readPermissions: Set<HKSampleType> {
-        Set([healthBGObject].compactMap { $0 }) }
-
     static var writePermissions: Set<HKSampleType> {
         Set([healthBGObject, healthCarbObject, healthFatObject, healthProteinObject, healthInsulinObject].compactMap { $0 }) }
 
@@ -47,8 +40,8 @@ public enum AppleHealthConfig {
     static let healthProteinObject = HKObjectType.quantityType(forIdentifier: .dietaryProtein)
     static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
 
-    // Meta-data key of FreeASPX data in HealthStore
-    static let freeAPSMetaKey = "From Trio"
+    // MetaDataKey of Trio data in HealthStore
+    static let TrioMetaDataKey = "TrioMetaDataKey"
 }
 
 final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDelegate, PumpHistoryDelegate {
@@ -78,7 +71,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
     private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
     private var lifetime = Lifetime()
 
-    // BG that will be return Publisher
+    // BG that will be returned by publisher
     @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
 
     // last anchor for HKAnchoredQuery
@@ -99,10 +92,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
     }
 
     var areAllowAllPermissions: Bool {
-        Set(AppleHealthConfig.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
-            .intersection([.notDetermined])
-            .isEmpty &&
-            Set(AppleHealthConfig.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
+        Set(AppleHealthConfig.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
             .intersection([.sharingDenied, .notDetermined])
             .isEmpty
     }
@@ -119,7 +109,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
         // loading only not FreeAPS bg
         // this predicate dont influence on Deleted Objects, only on added
         let predicateByMeta = HKQuery.predicateForObjects(
-            withMetadataKey: AppleHealthConfig.freeAPSMetaKey,
+            withMetadataKey: AppleHealthConfig.TrioMetaDataKey,
             operatorType: .notEqualTo,
             value: 1
         )
@@ -150,14 +140,11 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
         guard isAvailableOnCurrentDevice else {
             throw HKError.notAvailableOnCurrentDevice
         }
-        guard AppleHealthConfig.readPermissions.isNotEmpty, AppleHealthConfig.writePermissions.isNotEmpty else {
-            throw HKError.dataNotAvailable
-        }
 
         return try await withCheckedThrowingContinuation { continuation in
             healthKitStore.requestAuthorization(
                 toShare: AppleHealthConfig.writePermissions,
-                read: AppleHealthConfig.readPermissions
+                read: nil
             ) { status, error in
                 if let error = error {
                     continuation.resume(throwing: error)
@@ -196,7 +183,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
                         HKMetadataKeyExternalUUID: glucoseSample.id,
                         HKMetadataKeySyncIdentifier: glucoseSample.id,
                         HKMetadataKeySyncVersion: 1,
-                        AppleHealthConfig.freeAPSMetaKey: true
+                        AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
                     ]
                 )
             }
@@ -261,6 +248,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
             // Create HealthKit samples for carbs, fat, and protein
             for allSamples in carbs {
                 guard let id = allSamples.id else { continue }
+                let fpuID = allSamples.fpuID ?? id
 
                 let startDate = allSamples.actualDate ?? Date()
 
@@ -275,7 +263,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
                         HKMetadataKeyExternalUUID: id,
                         HKMetadataKeySyncIdentifier: id,
                         HKMetadataKeySyncVersion: 1,
-                        AppleHealthConfig.freeAPSMetaKey: true
+                        AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
                     ]
                 )
                 samples.append(carbSample)
@@ -288,10 +276,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
                         start: startDate,
                         end: startDate,
                         metadata: [
-                            HKMetadataKeyExternalUUID: id,
-                            HKMetadataKeySyncIdentifier: id,
+                            HKMetadataKeyExternalUUID: fpuID,
+                            HKMetadataKeySyncIdentifier: fpuID,
                             HKMetadataKeySyncVersion: 1,
-                            AppleHealthConfig.freeAPSMetaKey: true
+                            AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
                         ]
                     )
                     samples.append(fatSample)
@@ -305,10 +293,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
                         start: startDate,
                         end: startDate,
                         metadata: [
-                            HKMetadataKeyExternalUUID: id,
-                            HKMetadataKeySyncIdentifier: id,
+                            HKMetadataKeyExternalUUID: fpuID,
+                            HKMetadataKeySyncIdentifier: fpuID,
                             HKMetadataKeySyncVersion: 1,
-                            AppleHealthConfig.freeAPSMetaKey: true
+                            AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
                         ]
                     )
                     samples.append(proteinSample)
@@ -394,7 +382,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
                         HKMetadataKeySyncIdentifier: insulinSample.id,
                         HKMetadataKeySyncVersion: 1,
                         HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
-                        AppleHealthConfig.freeAPSMetaKey: true
+                        AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
                     ]
                 )
             }
@@ -511,155 +499,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
             }
         }
     }
-
-    // Observer that notifies when new Glucose values arrive in Apple Health
-
-    func createBGObserver() {
-        guard settingsManager.settings.useAppleHealth else { return }
-
-        guard let bgType = AppleHealthConfig.healthBGObject else {
-            warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
-            return
-        }
-
-        let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
-            guard let self = self else { return }
-            debug(.service, "Execute HealthKit observer query for loading increment samples")
-            guard observerError == nil else {
-                warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!)
-                return
-            }
-
-            if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
-                debug(.service, "Create increment query")
-                self.healthKitStore.execute(incrementQuery)
-            }
-        }
-        healthKitStore.execute(query)
-        debug(.service, "Create Observer for Blood Glucose")
-    }
-
-    func enableBackgroundDelivery() {
-        guard settingsManager.settings.useAppleHealth else {
-            healthKitStore.disableAllBackgroundDelivery { _, _ in }
-            return }
-
-        guard let bgType = AppleHealthConfig.healthBGObject else {
-            warning(
-                .service,
-                "Can not create background delivery, because unable to get the Blood Glucose type"
-            )
-            return
-        }
-
-        healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
-            guard error == nil else {
-                warning(.service, "Can not enable background delivery", error: error)
-                return
-            }
-            debug(.service, "Background delivery status is \(status)")
-        }
-    }
-
-    private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
-        guard let sampleType = AppleHealthConfig.healthBGObject else { return nil }
-
-        let query = HKAnchoredObjectQuery(
-            type: sampleType,
-            predicate: predicate,
-            anchor: lastBloodGlucoseQueryAnchor,
-            limit: HKObjectQueryNoLimit
-        ) { [weak self] _, addedObjects, _, anchor, _ in
-            guard let self = self else { return }
-            self.processQueue.async {
-                debug(.service, "AnchoredQuery did execute")
-
-                self.lastBloodGlucoseQueryAnchor = anchor
-
-                // Added objects
-                if let bgSamples = addedObjects as? [HKQuantitySample],
-                   bgSamples.isNotEmpty
-                {
-                    self.prepareBGSamplesToPublisherFetch(bgSamples)
-                }
-            }
-        }
-        return query
-    }
-
-    private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-
-        newGlucose += samples
-            .compactMap { sample -> HealthKitSample? in
-                let fromTrio = sample.metadata?[AppleHealthConfig.freeAPSMetaKey] as? Bool ?? false
-                guard !fromTrio else { return nil }
-                return HealthKitSample(
-                    healthKitId: sample.uuid.uuidString,
-                    date: sample.startDate,
-                    glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
-                )
-            }
-            .map { sample in
-                BloodGlucose(
-                    _id: sample.healthKitId,
-                    sgv: sample.glucose,
-                    direction: nil,
-                    date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
-                    dateString: sample.date,
-                    unfiltered: Decimal(sample.glucose),
-                    filtered: nil,
-                    noise: nil,
-                    glucose: sample.glucose,
-                    type: "sgv"
-                )
-            }
-            .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
-
-        newGlucose = newGlucose.removeDublicates()
-    }
-
-    // MARK: - GlucoseSource
-
-    var glucoseManager: FetchGlucoseManager?
-    var cgmManager: CGMManagerUI?
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future { [weak self] promise in
-            guard let self = self else {
-                promise(.success([]))
-                return
-            }
-
-            self.processQueue.async {
-                guard self.settingsManager.settings.useAppleHealth else {
-                    promise(.success([]))
-                    return
-                }
-
-                // Remove old BGs
-                self.newGlucose = self.newGlucose
-                    .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
-                // Get actual BGs (beetwen Date() - 1 day and Date())
-                let actualGlucose = self.newGlucose
-                    .filter { $0.dateString <= Date() }
-                // Update newGlucose
-                self.newGlucose = self.newGlucose
-                    .filter { !actualGlucose.contains($0) }
-
-                //  debug(.service, "Actual glucose is \(actualGlucose)")
-
-                //  debug(.service, "Current state of newGlucose is \(self.newGlucose)")
-
-                promise(.success(actualGlucose))
-            }
-        }
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        fetch(nil)
-    }
 }
 
 enum HealthKitPermissionRequestStatus {