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

Fix threading issue in TP Manager, Fix for Health Upload WiP (not yet working)

polscm32 1 год назад
Родитель
Сommit
4da52d489e

+ 13 - 6
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -467,12 +467,19 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         amount: event.bolus?.amount as Decimal?
                     )
                 case PumpEvent.tempBasal.rawValue:
-                    return PumpHistoryEvent(
-                        id: event.id ?? UUID().uuidString,
-                        type: .tempBasal,
-                        timestamp: event.timestamp ?? Date(),
-                        amount: event.tempBasal?.rate as Decimal?
-                    )
+                    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
                 }

+ 104 - 13
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -74,6 +74,11 @@ 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 }
 
@@ -331,31 +336,69 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               insulin.isNotEmpty
         else { return }
 
+        // Fetch existing temp basal entries from Core Data
+        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
+        )
+
+        var processedEvents: [PumpHistoryEvent] = []
+
+        await backgroundContext.perform {
+            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
+
+            for event in insulin {
+                switch event.type {
+                case .tempBasal:
+                    let tempBasalEvents = self.processTempBasalEvent(
+                        event,
+                        existingTempBasalEntries: existingTempBasalEntries
+                    )
+                    processedEvents.append(contentsOf: tempBasalEvents)
+
+                case .bolus:
+                    processedEvents.append(event)
+
+                default:
+                    break
+                }
+            }
+        }
+
+        // Create HKQuantitySamples for all processed events
         do {
-            let insulinSamples = insulin.compactMap { insulinSample -> HKQuantitySample? in
-                guard let insulinValue = insulinSample.amount else { return nil }
+            let insulinSamples = processedEvents.compactMap { processedEvent -> HKQuantitySample? in
+                guard let insulinValue = processedEvent.amount else { return nil }
 
-                // Determine the insulin delivery reason (bolus or basal)
                 let deliveryReason: HKInsulinDeliveryReason
-                switch insulinSample.type {
+                switch processedEvent.type {
                 case .bolus:
                     deliveryReason = .bolus
                 case .tempBasal:
                     deliveryReason = .basal
                 default:
-                    // Skip other types
-                    /// If deliveryReason is nil, the compactMap will filter this sample out preventing a crash
                     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: insulinSample.timestamp,
-                    end: insulinSample.timestamp,
+                    start: processedEvent.timestamp,
+                    end: endDate,
                     metadata: [
-                        HKMetadataKeyExternalUUID: insulinSample.id,
-                        HKMetadataKeySyncIdentifier: insulinSample.id,
+                        HKMetadataKeyExternalUUID: processedEvent.id,
+                        HKMetadataKeySyncIdentifier: processedEvent.id,
                         HKMetadataKeySyncVersion: 1,
                         HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
                         AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
@@ -368,18 +411,66 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                 return
             }
 
-            // Attempt to save the insulin samples to Apple Health
             try await healthKitStore.save(insulinSamples)
             debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
 
-            // After successful upload, update the isUploadedToHealth flag in Core Data
-            await updateInsulinAsUploaded(insulin)
+            await updateInsulinAsUploaded(processedEvents)
 
         } 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)
+                        }
+                    }
+                }
+
+                let newPumpHistoryEvent = PumpHistoryEvent(
+                    id: UUID().uuidString,
+                    type: .tempBasal,
+                    timestamp: event.timestamp,
+                    amount: value,
+                    duration: event.duration
+                )
+
+                processedTempBasalEvents.append(newPumpHistoryEvent)
+            }
+        }
+
+        return processedTempBasalEvents
+    }
+
     private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
         await backgroundContext.perform {
             let ids = insulin.map(\.id) as NSArray

+ 182 - 175
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -98,32 +98,32 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
     /// Registers handlers for Core Data changes
     private func registerHandlers() {
-            coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [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)
+                await self.uploadInsulin()
+            }
+        }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [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)
+                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
+        // 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 }
-                Task { [weak self] in
-                    guard let self = self else { return }
-                    await self.uploadGlucose()
-                }
-            }.store(in: &subscriptions)
-        }
+                await self.uploadGlucose()
+            }
+        }.store(in: &subscriptions)
+    }
 
     private func subscribe() {
         broadcaster.register(TempTargetsObserver.self, observer: self)
@@ -273,110 +273,112 @@ extension BaseTidepoolManager {
 /// Insulin Upload and Deletion Functionality
 extension BaseTidepoolManager {
     func uploadInsulin() async {
-        uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
+        await uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
     }
 
-    func uploadDose(_ events: [PumpHistoryEvent]) {
+    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 existingTempBasalEntries: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: backgroundContext,
-            predicate: NSPredicate.pumpHistoryLast24h,
+            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                NSPredicate.pumpHistoryLast24h,
+                NSPredicate(format: "tempBasal != nil")
+            ]),
             key: "timestamp",
             ascending: true,
             batchSize: 50
-        ).filter { $0.tempBasal != nil }
-
-        var 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: apsManager.pumpManager?.status.insulinType ?? nil,
-                    automatic: event.isSMB ?? true,
-                    manuallyEntered: event.isExternal ?? false
-                )
-
-                result.append(bolusDoseEntry)
+        )
 
-            default:
-                break
+        // 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
             }
 
-            return result
-        }
-
-        debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
+            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
+                    }
 
-        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
                 }
-
-                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
             }
-        }
 
-        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.")
-                    // After successful upload, update the isUploadedToTidepool flag in Core Data
-                    Task {
-                        let insulinEvents = events.filter {
-                            $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+            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)
                         }
-                        await self.updateInsulinAsUploaded(insulinEvents)
                     }
                 }
-            }
 
-            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.")
-                    // After successful upload, update the isUploadedToTidepool flag in Core Data
-                    Task {
-                        let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
-                        let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
+                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) }
 
-                        await self.updateInsulinAsUploaded(pumpEvents)
+                            await self.updateInsulinAsUploaded(pumpEvents)
+                        }
                     }
                 }
             }
@@ -432,83 +434,88 @@ extension BaseTidepoolManager {
 
 /// Insulin Helper Functions
 extension BaseTidepoolManager {
-    private func processTempBasalEvent(_ event: PumpHistoryEvent, existingTempBasalEntries: [PumpEventStored]) -> [DoseEntry] {
+    private func processTempBasalEvent(
+        _ event: PumpHistoryEvent,
+        existingTempBasalEntries: [PumpEventStored]
+    ) -> [DoseEntry] {
         var insulinDoseEvents: [DoseEntry] = []
 
-        // Loop through the pump history events
-        guard let duration = event.duration, let amount = event.amount,
-              let currentBasalRate = 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: apsManager.pumpManager?.status.insulinType ?? nil,
-                            automatic: true,
-                            manuallyEntered: false,
-                            isMutable: false
-                        )
-
-                        // Add the updated predecessor entry to the result
-                        insulinDoseEvents.append(updatedPredecessorEntry)
+        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)
+                        }
                     }
                 }
-            }
 
-            // Create a new dose entry for the current event
-            let currentEndDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration)))
-            let newDoseEntry = DoseEntry(
-                type: .tempBasal,
-                startDate: event.timestamp,
-                endDate: currentEndDate,
-                value: Double(value),
-                unit: .units,
-                deliveredUnits: Double(value),
-                syncIdentifier: event.id,
-                scheduledBasalRate: HKQuantity(
-                    unit: .internationalUnitsPerHour,
-                    doubleValue: Double(currentBasalRate.rate)
-                ),
-                insulinType: apsManager.pumpManager?.status.insulinType ?? nil,
-                automatic: true,
-                manuallyEntered: false,
-                isMutable: false
-            )
-
-            // Add the new event entry to the result
-            insulinDoseEvents.append(newDoseEntry)
+                // Create a new dose entry for the current event
+                let currentEndDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration)))
+                let newDoseEntry = DoseEntry(
+                    type: .tempBasal,
+                    startDate: event.timestamp,
+                    endDate: currentEndDate,
+                    value: Double(value),
+                    unit: .units,
+                    deliveredUnits: Double(value),
+                    syncIdentifier: event.id,
+                    scheduledBasalRate: HKQuantity(
+                        unit: .internationalUnitsPerHour,
+                        doubleValue: Double(currentBasalRate.rate)
+                    ),
+                    insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: false
+                )
+
+                // Add the new event entry to the result
+                insulinDoseEvents.append(newDoseEntry)
+            }
         }
 
         return insulinDoseEvents