소스 검색

Merge pull request #853 from kingst/backfill-cgm-support

[Part 2 of 2] CGM Backfill Support
Mike Plante 5 달 전
부모
커밋
c864ca8196
2개의 변경된 파일66개의 추가작업 그리고 21개의 파일을 삭제
  1. 9 11
      Trio/Sources/APS/FetchGlucoseManager.swift
  2. 57 10
      Trio/Sources/APS/Storage/GlucoseStorage.swift

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

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

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

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