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

Fix temp basal dosing math for Tidepool uploads

Deniz Cengiz 1 год назад
Родитель
Сommit
c1999cd645
1 измененных файлов с 113 добавлено и 105 удалено
  1. 113 105
      FreeAPS/Sources/Services/Network/TidepoolManager.swift

+ 113 - 105
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -60,7 +60,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         subscribe()
     }
 
-    /// load the Tidepool Remote Data Service if available
+    /// Loads the Tidepool service from saved state
     fileprivate func loadTidepoolManager() {
         if let rawTidepoolManager = rawTidepoolManager {
             tidepoolService = tidepoolServiceFromRaw(rawTidepoolManager)
@@ -69,61 +69,45 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
-    /// allows access to tidepoolService as a simple ServiceUI
+    /// Returns the Tidepool service UI if available
     func getTidepoolServiceUI() -> ServiceUI? {
-        if let tidepoolService = self.tidepoolService {
-            return tidepoolService as! any ServiceUI as ServiceUI
-        } else {
-            return nil
-        }
+        tidepoolService as? ServiceUI
     }
 
-    /// get the pluginHost of Tidepool
+    /// Returns the Tidepool plugin host
     func getTidepoolPluginHost() -> PluginHost? {
         self as PluginHost
     }
 
+    /// Adds a Tidepool service
     func addTidepoolService(service: Service) {
-        tidepoolService = service as! any RemoteDataService as RemoteDataService
+        tidepoolService = service as? RemoteDataService
     }
 
-    /// load the Tidepool Remote Data Service from raw storage
+    /// Loads the Tidepool service from raw stored data
     private func tidepoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
         guard let rawState = rawValue["state"] as? Service.RawStateValue,
               let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
-        else {
-            return nil
-        }
+        else { return nil }
+
         if let service = serviceType.init(rawState: rawState) {
-            return service as! any RemoteDataService as RemoteDataService
-        } else { return nil }
+            return service as? RemoteDataService
+        }
+        return nil
     }
 
+    /// Registers handlers for Core Data changes
     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()
-            }
+            Task { 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()
-            }
+            Task { 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()
-            }
+            Task { await self?.uploadGlucose() }
         }.store(in: &subscriptions)
     }
 
@@ -151,9 +135,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
                 tidepoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
                     switch result {
                     case let .failure(error):
-                        debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                        debug(.nightscout, "Error synchronizing carbs data with Tidepool: \(String(describing: error))")
                     case .success:
-                        debug(.nightscout, "Success synchronizing carbs data")
+                        debug(.nightscout, "Success synchronizing carbs data. Upload to Tidepool complete.")
                         // After successful upload, update the isUploadedToTidepool flag in Core Data
                         Task {
                             await self.updateCarbsAsUploaded(carbs)
@@ -211,9 +195,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
             tidepoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
                 switch result {
                 case let .failure(error):
-                    debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                    debug(.nightscout, "Error synchronizing carbs data with Tidepool: \(String(describing: error))")
                 case .success:
-                    debug(.nightscout, "Success synchronizing carbs data.")
+                    debug(.nightscout, "Success synchronizing carbs data. Upload to Tidepool complete.")
                 }
             }
         }
@@ -226,80 +210,22 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
     func uploadDose(_ events: [PumpHistoryEvent]) {
         guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
-        let tempBasalEventCount = events.filter { $0.type == .tempBasal }.count
-
-        // Fetch existing entries with an additional +1 count to get the previous entry as well -> need to update
-        var existingTempBasalEntries: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
+        // Fetch all temp basal entries from Core Data for the last 24 hours
+        let existingTempBasalEntries: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
             ofType: PumpEventStored.self,
             onContext: backgroundContext,
             predicate: NSPredicate.pumpHistoryLast24h,
             key: "timestamp",
-            ascending: false,
-            fetchLimit: tempBasalEventCount + 1,
+            ascending: true,
             batchSize: 50
         ).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.
-        if existingTempBasalEntries.isNotEmpty, existingTempBasalEntries.count > 1 {
-            existingTempBasalEntries.removeFirst()
-        }
-
-        let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
+        var insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
             var result = result
             switch event.type {
             case .tempBasal:
-                if let duration = event.duration, let amount = event.amount, let currentBasalRate = self.getCurrentBasalRate() {
-                    let value = (Decimal(duration) / 60.0) * amount
-
-                    // Create updated previous entry, if applicable -> update end date
-                    if let lastEntry = existingTempBasalEntries.first, let lastTimeStamp = lastEntry.timestamp {
-                        let lastEndDate = event.timestamp
-                        let lastDuration = lastEndDate.timeIntervalSince(lastTimeStamp) / 3600
-                        let lastDeliveredUnits = Double(lastDuration / 60.0) *
-                            Double(truncating: lastEntry.tempBasal?.rate ?? 0.0)
-
-                        let updatedLastEntry = DoseEntry(
-                            type: .tempBasal,
-                            startDate: lastTimeStamp,
-                            endDate: lastEndDate,
-                            value: lastDeliveredUnits,
-                            unit: .units,
-                            deliveredUnits: lastDeliveredUnits,
-                            syncIdentifier: lastEntry.id ?? UUID().uuidString,
-                            insulinType: apsManager.pumpManager?.status.insulinType ?? nil,
-                            automatic: true,
-                            manuallyEntered: false,
-                            isMutable: false
-                        )
-                        result.append(updatedLastEntry)
-                    }
-
-                    // Create new entry for current event
-                    let newDoseEntry = DoseEntry(
-                        type: .tempBasal,
-                        startDate: event.timestamp,
-                        endDate: event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration))),
-                        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
-                    )
-
-                    result.append(newDoseEntry)
-
-                    // Remove the first element from existingTempBasalEntries so that it does not get processed again
-                    if existingTempBasalEntries.isNotEmpty {
-                        existingTempBasalEntries.removeFirst()
-                    }
-                }
+                result
+                    .append(contentsOf: processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries))
 
             case .bolus:
                 let bolusDoseEntry = DoseEntry(
@@ -357,9 +283,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
             tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
                 switch result {
                 case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
+                    debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
                 case .success:
-                    debug(.nightscout, "Success synchronizing Dose data")
+                    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 {
@@ -373,9 +299,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
             tidepoolService.uploadPumpEventData(pumpEvents) { result in
                 switch result {
                 case let .failure(error):
-                    debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
+                    debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
                 case .success:
-                    debug(.nightscout, "Success synchronizing Pump Event data")
+                    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() }
@@ -388,6 +314,88 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
+    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)
+                    }
+                }
+            }
+
+            // 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)
+        }
+
+        return insulinDoseEvents
+    }
+
     private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
         await backgroundContext.perform {
             let ids = insulin.map(\.id) as NSArray
@@ -488,7 +496,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
-    /// force to uploads all data in Tidepool Service
+    /// Forces a full data upload to Tidepool
     func forceTidepoolDataUpload() {
         Task {
             await uploadInsulin()