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

TidePool implementation : Upload main data from oiaps

- upload glucose
- upload (and delete) carbs
- upload (and delete) dose basal and bolus
- upload event from pump

+ improve the deletion of TidePool.

(cherry picked from commit 8ced9096e381ad43d2357936edb31aff780e3f01)
Pierre L 2 лет назад
Родитель
Сommit
e8d834bef8

+ 2 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -29,6 +29,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var nightscoutManager: NightscoutManager!
+    @Injected() var tidePoolService: TidePoolManager!
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var healthKitManager: HealthKitManager!
@@ -206,6 +207,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         deviceDataManager.heartbeat(date: Date())
 
         nightscoutManager.uploadGlucose()
+        tidePoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
 
         let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
 

+ 13 - 0
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -1,4 +1,6 @@
 import Foundation
+import HealthKit
+import LoopKit
 
 struct BloodGlucose: JSON, Identifiable, Hashable {
     enum Direction: String, JSON {
@@ -91,3 +93,14 @@ extension BloodGlucose: SavitzkyGolaySmoothable {
         }
     }
 }
+
+extension BloodGlucose {
+    func convertStoredGlucoseSample(device: HKDevice?) -> StoredGlucoseSample {
+        StoredGlucoseSample(
+            syncIdentifier: id,
+            startDate: dateString.date,
+            quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
+            device: device
+        )
+    }
+}

+ 23 - 0
FreeAPS/Sources/Models/CarbsEntry.swift

@@ -1,4 +1,5 @@
 import Foundation
+import LoopKit
 
 struct CarbsEntry: JSON, Equatable, Hashable {
     let id: String?
@@ -36,3 +37,25 @@ extension CarbsEntry {
         case fpuID
     }
 }
+
+extension CarbsEntry {
+    func convertSyncCarb(operation: LoopKit.Operation = .create) -> SyncCarbObject {
+        SyncCarbObject(
+            absorptionTime: nil,
+            createdByCurrentApp: true,
+            foodType: nil,
+            grams: Double(carbs),
+            startDate: createdAt,
+            uuid: UUID(uuidString: id!),
+            provenanceIdentifier: enteredBy ?? "",
+            syncIdentifier: id,
+            syncVersion: nil,
+            userCreatedDate: nil,
+            userUpdatedDate: nil,
+            userDeletedDate: nil,
+            operation: operation,
+            addedDate: nil,
+            supercededDate: nil
+        )
+    }
+}

+ 27 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -1,4 +1,5 @@
 import Foundation
+import LoopKit
 
 struct PumpHistoryEvent: JSON, Equatable {
     let id: String
@@ -82,3 +83,29 @@ extension PumpHistoryEvent {
         case note
     }
 }
+
+extension EventType {
+    func mapEventTypeToPumpEventType() -> PumpEventType? {
+        switch self {
+        case .prime:
+            return PumpEventType.prime
+        case .pumpResume:
+            return PumpEventType.resume
+        case .rewind:
+            return PumpEventType.rewind
+        case .pumpSuspend:
+            return PumpEventType.suspend
+        case .nsBatteryChange,
+             .pumpBattery:
+            return PumpEventType.replaceComponent(componentType: .pump)
+        case .nsInsulinChange:
+            return PumpEventType.replaceComponent(componentType: .reservoir)
+        case .nsSiteChange:
+            return PumpEventType.replaceComponent(componentType: .infusionSet)
+        case .pumpAlarm:
+            return PumpEventType.alarm
+        default:
+            return nil
+        }
+    }
+}

+ 13 - 0
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -8,6 +8,7 @@ extension DataTable {
         @Injected() var carbsStorage: CarbsStorage!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var healthkitManager: HealthKitManager!
+        @Injected() var tidePoolManager: TidePoolManager!
 
         func pumpHistory() -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent()
@@ -26,6 +27,16 @@ extension DataTable {
         }
 
         func deleteCarbs(_ treatement: Treatment) {
+            // need to start with tidePool because Nightscout delete data
+            // probably to revise the logic
+            // TODO:
+            tidePoolManager.deleteCarbs(
+                at: treatement.date,
+                isFPU: treatement.isFPU,
+                fpuID: treatement.fpuID,
+                syncID: treatement.id
+            )
+
             nightscoutManager.deleteCarbs(
                 at: treatement.date,
                 isFPU: treatement.isFPU,
@@ -35,6 +46,8 @@ extension DataTable {
         }
 
         func deleteInsulin(_ treatement: Treatment) {
+            // delete tidePoolManager before NS - TODO
+            tidePoolManager.deleteInsulin(at: treatement.date)
             nightscoutManager.deleteInsulin(at: treatement.date)
             if let id = treatement.idPumpEvent {
                 healthkitManager.deleteInsulin(syncID: id)

+ 266 - 18
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -1,5 +1,6 @@
 import Combine
 import Foundation
+import HealthKit
 import LoopKit
 import LoopKitUI
 import Swinject
@@ -10,29 +11,32 @@ protocol TidePoolManager {
     func getTidePoolPluginHost() -> PluginHost?
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
-    func uploadStatus()
-    func uploadGlucose()
-    func uploadStatistics(dailystat: Statistics)
-    func uploadPreferences(_ preferences: Preferences)
-    func uploadProfileAndSettings(_: Bool)
+//    func uploadStatus()
+    func uploadGlucose(device: HKDevice?)
+//    func uploadStatistics(dailystat: Statistics)
+//    func uploadPreferences(_ preferences: Preferences)
+//    func uploadProfileAndSettings(_: Bool)
 }
 
 final class BaseTidePoolManager: TidePoolManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var pluginManager: PluginManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var storage: FileStorage!
+    @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
-    private var ping: TimeInterval?
     private var tidePoolService: RemoteDataService? {
         didSet {
             if let tidePoolService = tidePoolService {
                 rawTidePoolManager = tidePoolService.rawValue
+            } else {
+                rawTidePoolManager = nil
             }
         }
     }
 
-    private var lifetime = Lifetime()
-
     @PersistedProperty(key: "TidePoolState") var rawTidePoolManager: Service.RawValue?
 
     init(resolver: Resolver) {
@@ -59,6 +63,7 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
         }
     }
 
+    /// get the pluginHost of TidePool
     func getTidePoolPluginHost() -> PluginHost? {
         self as PluginHost
     }
@@ -89,27 +94,268 @@ final class BaseTidePoolManager: TidePoolManager, Injectable {
         nil
     }
 
-    func deleteCarbs(at _: Date, isFPU _: Bool?, fpuID _: String?, syncID _: String) {}
+    func uploadCarbs() {
+        let carbs: [CarbsEntry] = carbsStorage.recent()
+
+        guard !carbs.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        processQueue.async {
+            carbs.chunks(ofCount: tidePoolService.carbDataLimit ?? 100).forEach { chunk in
+
+                let syncCarb: [SyncCarbObject] = Array(chunk).map {
+                    $0.convertSyncCarb()
+                }
+                tidePoolService.uploadCarbData(created: syncCarb, updated: [], deleted: []) { result in
+                    switch result {
+                    case let .failure(error):
+                        debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                    case .success:
+                        debug(.nightscout, "Success synchronizing carbs data:")
+                    }
+                }
+            }
+        }
+    }
+
+    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID _: String) {
+        guard let tidePoolService = self.tidePoolService else { return }
 
-    func deleteInsulin(at _: Date) {}
+        processQueue.async {
+            var carbsToDelete: [CarbsEntry] = []
+            let allValues = self.storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
 
-    func uploadStatus() {}
+            if let isFPU = isFPU, isFPU {
+                guard let fpuID = fpuID else { return }
+                carbsToDelete = allValues.filter { $0.fpuID == fpuID }.removeDublicates()
+            } else {
+                carbsToDelete = allValues.filter { $0.createdAt == date }.removeDublicates()
+            }
 
-    func uploadGlucose() {}
+            let syncCarb = carbsToDelete.map { d in
+                d.convertSyncCarb(operation: .delete)
+            }
 
-    func uploadStatistics(dailystat _: Statistics) {}
+            tidePoolService.uploadCarbData(created: [], updated: [], deleted: syncCarb) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing carbs data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing carbs data:")
+                }
+            }
+        }
+    }
 
-    func uploadPreferences(_: Preferences) {}
+    func deleteInsulin(at d: Date) {
+        let allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
 
-    func uploadProfileAndSettings(_: Bool) {}
+        guard !allValues.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        var doseDataToDelete: [DoseEntry] = []
+
+        guard let entry = allValues.first(where: { $0.timestamp == d }) else {
+            return
+        }
+        doseDataToDelete
+            .append(DoseEntry(
+                type: .bolus,
+                startDate: entry.timestamp,
+                value: Double(entry.amount!),
+                unit: .units,
+                syncIdentifier: entry.id
+            ))
+
+        processQueue.async {
+            tidePoolService.uploadDoseData(created: [], deleted: doseDataToDelete) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Dose delete data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Dose delete data:")
+                }
+            }
+        }
+    }
+
+    func uploadDose() {
+        let events = pumpHistoryStorage.recent()
+        guard !events.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        let eventsBasal = events.filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
+            .sorted { $0.timestamp < $1.timestamp }
+
+        let doseDataBasal: [DoseEntry] = eventsBasal.reduce([]) { result, event in
+            var result = result
+            switch event.type {
+            case .tempBasal:
+                // update the previous tempBasal with endtime = starttime of the last event
+                if let last: DoseEntry = result.popLast() {
+                    let value = max(
+                        0,
+                        Double(event.timestamp.timeIntervalSince1970 - last.startDate.timeIntervalSince1970) / 3600
+                    ) *
+                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
+                    result.append(DoseEntry(
+                        type: .tempBasal,
+                        startDate: last.startDate,
+                        endDate: event.timestamp,
+                        value: value,
+                        unit: last.unit,
+                        deliveredUnits: value,
+                        syncIdentifier: last.syncIdentifier,
+                        // scheduledBasalRate: last.scheduledBasalRate,
+                        insulinType: last.insulinType,
+                        automatic: last.automatic,
+                        manuallyEntered: last.manuallyEntered
+                    ))
+                }
+                result.append(DoseEntry(
+                    type: .tempBasal,
+                    startDate: event.timestamp,
+                    value: 0.0,
+                    unit: .unitsPerHour,
+                    syncIdentifier: event.id,
+                    scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(event.rate!)),
+                    insulinType: nil,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: true
+                ))
+            case .tempBasalDuration:
+                if let last: DoseEntry = result.popLast(),
+                   last.type == .tempBasal,
+                   last.startDate == event.timestamp
+                {
+                    let durationMin = event.durationMin ?? 0
+                    // result.append(last)
+                    let value = (Double(durationMin) / 60.0) *
+                        (last.scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour) ?? 0.0)
+                    result.append(DoseEntry(
+                        type: .tempBasal,
+                        startDate: last.startDate,
+                        endDate: Calendar.current.date(byAdding: .minute, value: durationMin, to: last.startDate) ?? last
+                            .startDate,
+                        value: value,
+                        unit: last.unit,
+                        deliveredUnits: value,
+                        syncIdentifier: last.syncIdentifier,
+                        scheduledBasalRate: last.scheduledBasalRate,
+                        insulinType: last.insulinType,
+                        automatic: last.automatic,
+                        manuallyEntered: last.manuallyEntered
+                    ))
+                }
+            default: break
+            }
+            return result
+        }
+
+        let boluses: [DoseEntry] = events.compactMap { event -> DoseEntry? in
+            switch event.type {
+            case .bolus:
+                return DoseEntry(
+                    type: .bolus,
+                    startDate: event.timestamp,
+                    endDate: event.timestamp,
+                    value: Double(event.amount!),
+                    unit: .units,
+                    deliveredUnits: nil,
+                    syncIdentifier: event.id,
+                    scheduledBasalRate: nil,
+                    insulinType: nil,
+                    automatic: true,
+                    manuallyEntered: false
+                )
+            default: return 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
+            }
+        }
+
+        processQueue.async {
+            tidePoolService.uploadDoseData(created: doseDataBasal + boluses, deleted: []) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Dose data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Dose data:")
+                }
+            }
+
+            tidePoolService.uploadPumpEventData(pumpEvents) { result in
+                switch result {
+                case let .failure(error):
+                    debug(.nightscout, "Error synchronizing Pump Event data: \(String(describing: error))")
+                case .success:
+                    debug(.nightscout, "Success synchronizing Pump Event data:")
+                }
+            }
+        }
+    }
+
+    func uploadGlucose(device: HKDevice?) {
+        let glucose: [BloodGlucose] = glucoseStorage.recent()
+
+        guard !glucose.isEmpty, let tidePoolService = self.tidePoolService else { return }
+
+        let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id) != nil }
+
+        processQueue.async {
+            glucoseWithoutCorrectID.chunks(ofCount: tidePoolService.glucoseDataLimit ?? 100)
+                .forEach { chunk in
+                    // all glucose attached with the current device ;-(
+
+                    let chunkStoreGlucose = Array(chunk).map {
+                        $0.convertStoredGlucoseSample(device: device)
+                    }
+                    tidePoolService.uploadGlucoseData(chunkStoreGlucose) { result in
+                        switch result {
+                        case let .failure(error):
+                            debug(.nightscout, "Error synchronizing glucose data: \(String(describing: error))")
+                        // self.uploadFailed(key)
+                        case .success:
+                            debug(.nightscout, "Success synchronizing glucose data:")
+                        }
+                    }
+                }
+        }
+    }
 }
 
 extension BaseTidePoolManager: PumpHistoryObserver {
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {}
+    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
+        uploadDose()
+    }
 }
 
 extension BaseTidePoolManager: CarbsObserver {
-    func carbsDidUpdate(_: [CarbsEntry]) {}
+    func carbsDidUpdate(_: [CarbsEntry]) {
+        uploadCarbs()
+    }
 }
 
 extension BaseTidePoolManager: TempTargetsObserver {
@@ -154,7 +400,9 @@ extension BaseTidePoolManager: ServiceDelegate {
 extension BaseTidePoolManager: StatefulPluggableDelegate {
     func pluginDidUpdateState(_: LoopKit.StatefulPluggable) {}
 
-    func pluginWantsDeletion(_: LoopKit.StatefulPluggable) {}
+    func pluginWantsDeletion(_: LoopKit.StatefulPluggable) {
+        tidePoolService = nil
+    }
 }
 
 // Service extension for rawValue