소스 검색

Fix Basal Calculation, Refactoring

polscm32 1 년 전
부모
커밋
faa23a0786
1개의 변경된 파일133개의 추가작업 그리고 94개의 파일을 삭제
  1. 133 94
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

+ 133 - 94
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -74,11 +74,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
 
         registerHandlers()
 
-        Task { [weak self] in
-            guard let self = self else { return }
-            await self.uploadInsulin()
-        }
-
         guard isAvailableOnCurrentDevice,
               AppleHealthConfig.healthBGObject != nil else { return }
 
@@ -336,7 +331,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               insulin.isNotEmpty
         else { return }
 
-        // Fetch existing temp basal entries from Core Data
+        // Fetch existing temp basal entries from Core Data for the last 24 hours
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: backgroundContext,
@@ -349,126 +344,170 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             batchSize: 50
         )
 
-        var processedEvents: [PumpHistoryEvent] = []
+        // Ensure that the fetched results are of the expected type
+        guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
 
-        await backgroundContext.perform {
-            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
+        // Create a mapping from timestamps to indices for quick access to existing entries
+        let existingEntriesByTimestamp = Dictionary(
+            uniqueKeysWithValues: existingTempBasalEntries.enumerated()
+                .map { ($0.element.timestamp, $0.offset) }
+        )
 
+        // Initialize an array to hold the HealthKit samples to be uploaded
+        var insulinSamples: [HKQuantitySample] = []
+
+        // Perform the data processing on the background context
+        await backgroundContext.perform {
             for event in insulin {
                 switch event.type {
+                case .bolus:
+                    // For bolus events, create a HealthKit sample directly
+                    if let sample = self.createSample(for: event, sampleType: sampleType) {
+                        insulinSamples.append(sample)
+                    }
+
                 case .tempBasal:
-                    let tempBasalEvents = self.processTempBasalEvent(
-                        event,
-                        existingTempBasalEntries: existingTempBasalEntries
-                    )
-                    processedEvents.append(contentsOf: tempBasalEvents)
+                    // For temp basal events, process them and adjust overlapping durations if necessary
+                    guard let duration = event.duration, let amount = event.amount else { continue }
+
+                    // Calculate the total insulin delivered during the temp basal period
+                    let value = (Decimal(duration) / 60.0) * amount
+
+                    // Check if there's a matching existing temp basal entry
+                    if let matchingEntryIndex = existingEntriesByTimestamp[event.timestamp] {
+                        let predecessorIndex = matchingEntryIndex - 1
+                        if predecessorIndex >= 0 {
+                            // Get the predecessor entry to handle overlapping temp basal events
+                            let predecessorEntry = existingTempBasalEntries[predecessorIndex]
+
+                            // Adjust the predecessor entry if it overlaps with the current event
+                            if let adjustedSample = self.processPredecessorEntry(
+                                predecessorEntry,
+                                nextEventTimestamp: event.timestamp,
+                                sampleType: sampleType
+                            ) {
+                                insulinSamples.append(adjustedSample)
+                            }
+                        }
 
-                case .bolus:
-                    processedEvents.append(event)
+                        // Create a new PumpHistoryEvent with the calculated insulin value
+                        let newEvent = PumpHistoryEvent(
+                            id: event.id,
+                            type: .tempBasal,
+                            timestamp: event.timestamp,
+                            amount: value,
+                            duration: event.duration
+                        )
+
+                        // Create a HealthKit sample for the current temp basal event
+                        if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
+                            insulinSamples.append(sample)
+                        }
+                    }
 
                 default:
+                    // Ignore other event types
                     break
                 }
             }
         }
 
-        // Create HKQuantitySamples for all processed events
+        // Save the processed insulin samples to HealthKit
         do {
-            let insulinSamples = processedEvents.compactMap { processedEvent -> HKQuantitySample? in
-                guard let insulinValue = processedEvent.amount else { return nil }
-
-                let deliveryReason: HKInsulinDeliveryReason
-                switch processedEvent.type {
-                case .bolus:
-                    deliveryReason = .bolus
-                case .tempBasal:
-                    deliveryReason = .basal
-                default:
-                    return nil
-                }
-
-                // adjust end date based on duration
-                let endDate = processedEvent.timestamp
-                    .addingTimeInterval(TimeInterval(minutes: Double(processedEvent.duration ?? 0)))
-
-                return HKQuantitySample(
-                    type: sampleType,
-                    quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double(insulinValue)),
-                    start: processedEvent.timestamp,
-                    end: endDate,
-                    metadata: [
-                        HKMetadataKeyExternalUUID: processedEvent.id,
-                        HKMetadataKeySyncIdentifier: processedEvent.id,
-                        HKMetadataKeySyncVersion: 1,
-                        HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
-                        AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
-                    ]
-                )
-            }
-
             guard insulinSamples.isNotEmpty else {
                 debug(.service, "No insulin samples available for upload.")
                 return
             }
 
+            // Attempt to save the samples to HealthKit
             try await healthKitStore.save(insulinSamples)
             debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
 
-            await updateInsulinAsUploaded(processedEvents)
+            // Mark the insulin events as uploaded
+            await updateInsulinAsUploaded(insulin)
 
         } catch {
             debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
         }
     }
 
-    private func processTempBasalEvent(
-        _ event: PumpHistoryEvent,
-        existingTempBasalEntries: [PumpEventStored]
-    ) -> [PumpHistoryEvent] {
-        var processedTempBasalEvents: [PumpHistoryEvent] = []
-
-        backgroundContext.performAndWait {
-            guard let duration = event.duration, let amount = event.amount else { return }
-            let value = (Decimal(duration) / 60.0) * amount
-
-            if let matchingEntryIndex = existingTempBasalEntries.firstIndex(where: { $0.timestamp == event.timestamp }) {
-                let predecessorIndex = matchingEntryIndex - 1
-                if predecessorIndex >= 0 {
-                    let predecessorEntry = existingTempBasalEntries[predecessorIndex]
-
-                    if let predecessorTimestamp = predecessorEntry.timestamp {
-                        let predecessorEndDate = predecessorTimestamp
-                            .addingTimeInterval(TimeInterval(Int(predecessorEntry.tempBasal?.duration ?? 0) * 60))
-                        if predecessorEndDate > event.timestamp {
-                            let adjustedEndDate = event.timestamp
-                            let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
-
-                            let adjustedPumpHistoryEvent = PumpHistoryEvent(
-                                id: UUID().uuidString,
-                                type: .tempBasal,
-                                timestamp: predecessorTimestamp,
-                                amount: Decimal(adjustedDuration / 3600) * amount,
-                                duration: Int(adjustedDuration / 60)
-                            )
-
-                            processedTempBasalEvents.append(adjustedPumpHistoryEvent)
-                        }
-                    }
-                }
+    // Helper function to create a HealthKit sample from a PumpHistoryEvent
+    private func createSample(for event: PumpHistoryEvent, sampleType: HKQuantityType) -> HKQuantitySample? {
+        // Ensure the event has a valid insulin amount
+        guard let insulinValue = event.amount else { return nil }
+
+        // Determine the insulin delivery reason based on the event type
+        let deliveryReason: HKInsulinDeliveryReason
+        switch event.type {
+        case .bolus:
+            deliveryReason = .bolus
+        case .tempBasal:
+            deliveryReason = .basal
+        default:
+            return nil
+        }
 
-                let newPumpHistoryEvent = PumpHistoryEvent(
-                    id: UUID().uuidString,
-                    type: .tempBasal,
-                    timestamp: event.timestamp,
-                    amount: value,
-                    duration: event.duration
-                )
+        // Calculate the end date based on the event duration
+        let endDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(event.duration ?? 0)))
+
+        // Create the HealthKit quantity sample with the appropriate metadata
+        let sample = HKQuantitySample(
+            type: sampleType,
+            quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double(insulinValue)),
+            start: event.timestamp,
+            end: endDate,
+            metadata: [
+                HKMetadataKeyExternalUUID: event.id,
+                HKMetadataKeySyncIdentifier: event.id,
+                HKMetadataKeySyncVersion: 1,
+                HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
+                AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
+            ]
+        )
 
-                processedTempBasalEvents.append(newPumpHistoryEvent)
-            }
+        return sample
+    }
+
+    // Helper function to process a predecessor temp basal entry and adjust overlapping durations
+    private func processPredecessorEntry(
+        _ predecessorEntry: PumpEventStored,
+        nextEventTimestamp: Date,
+        sampleType: HKQuantityType
+    ) -> HKQuantitySample? {
+        // Ensure the predecessor entry has the necessary data
+        guard let predecessorTimestamp = predecessorEntry.timestamp,
+              let predecessorEntryId = predecessorEntry.id else { return nil }
+
+        // Calculate the original end date of the predecessor temp basal
+        let predecessorDurationMinutes = predecessorEntry.tempBasal?.duration ?? 0
+        let predecessorEndDate = predecessorTimestamp.addingTimeInterval(TimeInterval(Int(predecessorDurationMinutes) * 60))
+
+        // Check if the predecessor temp basal overlaps with the next event
+        if predecessorEndDate > nextEventTimestamp {
+            // Adjust the end date to the start of the next event to prevent overlap
+            let adjustedEndDate = nextEventTimestamp
+            let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
+
+            // Calculate the insulin rate and adjusted delivered units
+            let predecessorEntryRate = predecessorEntry.tempBasal?.rate?.decimalValue ?? 0
+            let adjustedDurationHours = Decimal(adjustedDuration) / 3600
+            let adjustedDeliveredUnits = adjustedDurationHours * predecessorEntryRate
+
+            // Create a new PumpHistoryEvent with the adjusted values
+            let adjustedEvent = PumpHistoryEvent(
+                id: predecessorEntryId,
+                type: .tempBasal,
+                timestamp: predecessorTimestamp,
+                amount: adjustedDeliveredUnits,
+                duration: Int(adjustedDuration / 60)
+            )
+
+            // Create and return the HealthKit sample for the adjusted event
+            return createSample(for: adjustedEvent, sampleType: sampleType)
         }
 
-        return processedTempBasalEvents
+        // If there is no overlap, no adjustment is needed
+        return nil
     }
 
     private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {