Преглед изворни кода

Add migration logic for pumpHistory and carbHistory

polscm32 aka Marvout пре 1 година
родитељ
комит
97cdd64725

+ 6 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -322,6 +322,7 @@
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
 		BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */; };
+		BD793CC52CE8ABAD00D669AC /* MigrationScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD793CC42CE8ABA700D669AC /* MigrationScript.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
@@ -999,6 +1000,7 @@
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
 		BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetConfiguration.swift; sourceTree = "<group>"; };
+		BD793CC42CE8ABA700D669AC /* MigrationScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScript.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
@@ -2323,11 +2325,12 @@
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			children = (
-				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
-				5825D1622BD405AE00F36E9B /* Helper */,
 				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
+				BD793CC42CE8ABA700D669AC /* MigrationScript.swift */,
+				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
+				5825D1622BD405AE00F36E9B /* Helper */,
 			);
 			path = Model;
 			sourceTree = "<group>";
@@ -3267,6 +3270,7 @@
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
+				BD793CC52CE8ABAD00D669AC /* MigrationScript.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,

+ 12 - 2
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -59,18 +59,28 @@ import Swinject
     init() {
         debug(
             .default,
-            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(BuildDetails.default.buildDate())] [buildExpires: \(BuildDetails.default.calculateExpirationDate())]"
+            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))]"
         )
         loadServices()
 
         // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
         cleanupOldData()
+
+        importStuff()
+    }
+
+    func importStuff() {
+        Task {
+            let importer = JSONImporter(context: coreDataStack.newTaskContext())
+            await importer.importPumpHistoryIfNeeded()
+            await importer.importCarbHistoryIfNeeded()
+        }
     }
 
     var body: some Scene {
         WindowGroup {
             Main.RootView(resolver: resolver)
-                .preferredColorScheme(colorScheme(for: colorSchemePreference ?? .systemDefault) ?? nil)
+                .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)

+ 1 - 1
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -28,6 +28,6 @@ extension TrioRemoteControl {
     }
 
     private func isRunningInAPNSProductionEnvironment() -> Bool {
-        return BuildDetails.default.isTestFlightBuild()
+        BuildDetails.default.isTestFlightBuild()
     }
 }

+ 11 - 0
Model/Helper/CarbEntryStored+helper.swift

@@ -65,6 +65,17 @@ extension CarbEntryStored {
     }
 }
 
+struct CarbEntryDTO: Codable {
+    var id: UUID?
+    var carbs: Double
+    var date: Date?
+    var fat: Double?
+    var protein: Double?
+    var isFPU: Bool?
+    var note: String?
+    var enteredBy: String?
+}
+
 extension CarbEntryStored: Encodable {
     enum CodingKeys: String, CodingKey {
         case actualDate

+ 84 - 3
Model/Helper/PumpEvent+helper.swift

@@ -100,8 +100,8 @@ struct BolusDTO: Codable {
     var timestamp: String
     var amount: Double
     var isExternal: Bool
-    var isSMB: Bool
-    var duration: Int
+    var isSMB: Bool?
+    var duration: Int?
     var _type: String = "Bolus"
 }
 
@@ -127,11 +127,19 @@ struct TempBasalDurationDTO: Codable {
     }
 }
 
+struct PumpSuspendDTO: Codable {
+    var id: String
+    var timestamp: String
+    var reason: String?
+    var _type: String = "PumpSuspend"
+}
+
 // Mask distinct DTO subtypes with a common enum that conforms to Encodable
-enum PumpEventDTO: Encodable {
+enum PumpEventDTO: Encodable, Decodable {
     case bolus(BolusDTO)
     case tempBasal(TempBasalDTO)
     case tempBasalDuration(TempBasalDurationDTO)
+    case pumpSuspend(PumpSuspendDTO)
 
     func encode(to encoder: Encoder) throws {
         switch self {
@@ -141,8 +149,81 @@ enum PumpEventDTO: Encodable {
             try tempBasal.encode(to: encoder)
         case let .tempBasalDuration(tempBasalDuration):
             try tempBasalDuration.encode(to: encoder)
+        case let .pumpSuspend(pumpSuspend):
+            try pumpSuspend.encode(to: encoder)
+        }
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        guard let type = try? container.decode(String.self, forKey: ._type) else {
+            throw DecodingError.keyNotFound(
+                CodingKeys._type,
+                DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "_type key not found")
+            )
+        }
+
+        switch type {
+        case "Bolus":
+            do {
+                let bolus = try BolusDTO(from: decoder)
+                self = .bolus(bolus)
+            } catch {
+                throw DecodingError.typeMismatch(
+                    BolusDTO.self,
+                    DecodingError
+                        .Context(
+                            codingPath: decoder.codingPath,
+                            debugDescription: "Failed to decode BolusDTO",
+                            underlyingError: error
+                        )
+                )
+            }
+        case "TempBasal":
+            do {
+                let tempBasal = try TempBasalDTO(from: decoder)
+                self = .tempBasal(tempBasal)
+            } catch {
+                throw DecodingError.typeMismatch(
+                    TempBasalDTO.self,
+                    DecodingError
+                        .Context(
+                            codingPath: decoder.codingPath,
+                            debugDescription: "Failed to decode TempBasalDTO",
+                            underlyingError: error
+                        )
+                )
+            }
+        case "TempBasalDuration":
+            do {
+                let tempBasalDuration = try TempBasalDurationDTO(from: decoder)
+                self = .tempBasalDuration(tempBasalDuration)
+            } catch {
+                throw DecodingError.typeMismatch(
+                    TempBasalDurationDTO.self,
+                    DecodingError
+                        .Context(
+                            codingPath: decoder.codingPath,
+                            debugDescription: "Failed to decode TempBasalDurationDTO",
+                            underlyingError: error
+                        )
+                )
+            }
+        case "PumpSuspend":
+            let pumpSuspend = try PumpSuspendDTO(from: decoder)
+            self = .pumpSuspend(pumpSuspend)
+        default:
+            throw DecodingError.dataCorruptedError(
+                forKey: ._type,
+                in: container,
+                debugDescription: "Unbekannter _type-Wert: \(type)"
+            )
         }
     }
+
+    private enum CodingKeys: String, CodingKey {
+        case _type
+    }
 }
 
 // Extension with helper functions to map pump events to DTO objects via uniform masking enum

+ 169 - 0
Model/MigrationScript.swift

@@ -0,0 +1,169 @@
+import CoreData
+import Foundation
+
+class JSONImporter {
+    private let context: NSManagedObjectContext
+    private let fileManager = FileManager.default
+
+    init(context: NSManagedObjectContext) {
+        self.context = context
+    }
+
+    func importPumpHistoryIfNeeded() async {
+        let userDefaultsKey = "pumpHistoryImported"
+        let hasImported = UserDefaults.standard.bool(forKey: userDefaultsKey)
+
+        guard !hasImported else {
+            debugPrint("Pump history already imported. Skipping import.")
+            return
+        }
+
+        do {
+            // Get filepath
+            guard let filePath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
+                .first?
+                .appendingPathComponent(OpenAPS.Monitor.pumpHistory),
+                fileManager.fileExists(atPath: filePath.path)
+            else {
+                debugPrint("Pump history file not found at path \(OpenAPS.Monitor.pumpHistory)")
+                return
+            }
+
+            // Read JSON and decode
+            let data = try Data(contentsOf: filePath)
+            let pumpEvents = try JSONDecoder().decode([PumpEventDTO].self, from: data)
+
+            // Save to Core Data
+            await context.perform {
+                for event in pumpEvents {
+                    self.storePumpEventFromDTO(event)
+                }
+
+                do {
+                    guard self.context.hasChanges else { return }
+                    try self.context.save()
+                    debugPrint("\(DebuggingIdentifiers.succeeded) Pump history successfully imported into Core Data.")
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to save pump history to Core Data: \(error)")
+                }
+            }
+
+            // Delete JSON
+            try fileManager.removeItem(at: filePath)
+            debugPrint("pumphistory.json deleted after successful import.")
+
+            // Update UserDefaults flag
+            UserDefaults.standard.set(true, forKey: userDefaultsKey)
+        } catch {
+            debugPrint("Error importing pump history: \(error)")
+        }
+    }
+
+    private func storePumpEventFromDTO(_ event: PumpEventDTO) {
+        // Map each type of PumpEventDTO to its corresponding Core Data model.
+        switch event {
+        case let .bolus(bolusDTO):
+            let pumpEvent = PumpEventStored(context: context)
+            pumpEvent.id = bolusDTO.id
+            pumpEvent.timestamp = ISO8601DateFormatter().date(from: bolusDTO.timestamp)
+            pumpEvent.type = bolusDTO._type
+
+            let bolus = BolusStored(context: context)
+            bolus.amount = NSDecimalNumber(value: bolusDTO.amount)
+            bolus.isExternal = bolusDTO.isExternal
+            bolus.isSMB = bolusDTO.isSMB ?? false
+            pumpEvent.bolus = bolus
+
+        case let .tempBasal(tempBasalDTO):
+            let pumpEvent = PumpEventStored(context: context)
+            pumpEvent.id = tempBasalDTO.id
+            pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDTO.timestamp)
+            pumpEvent.type = tempBasalDTO._type
+
+            let tempBasal = TempBasalStored(context: context)
+            tempBasal.tempType = tempBasalDTO.temp
+            tempBasal.rate = NSDecimalNumber(value: tempBasalDTO.rate)
+            pumpEvent.tempBasal = tempBasal
+
+        case let .tempBasalDuration(tempBasalDurationDTO):
+            let pumpEvent = PumpEventStored(context: context)
+            pumpEvent.id = tempBasalDurationDTO.id
+            pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDurationDTO.timestamp)
+            pumpEvent.type = tempBasalDurationDTO._type
+
+            let tempBasal = TempBasalStored(context: context)
+            tempBasal.duration = Int16(tempBasalDurationDTO.duration)
+            pumpEvent.tempBasal = tempBasal
+        case .pumpSuspend:
+            return
+        }
+    }
+
+    func importCarbHistoryIfNeeded() async {
+        let userDefaultsKey = "carbHistoryImported"
+        let hasImported = UserDefaults.standard.bool(forKey: userDefaultsKey)
+
+        guard !hasImported else {
+            debugPrint("Carb history already imported. Skipping import.")
+            return
+        }
+
+        do {
+            // Get filepath
+            guard let filePath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
+                .first?
+                .appendingPathComponent(OpenAPS.Monitor.carbHistory),
+                fileManager.fileExists(atPath: filePath.path)
+            else {
+                debugPrint("Carb history file not found at path \(OpenAPS.Monitor.carbHistory)")
+                return
+            }
+
+            // Read JSON and decode
+            let data = try Data(contentsOf: filePath)
+            let decoder = JSONDecoder()
+            decoder.dateDecodingStrategy = .iso8601
+
+            // Decode JSON
+            let carbEntries = try decoder.decode([CarbEntryDTO].self, from: data)
+
+            // Save to Core Data
+            await context.perform {
+                for entryDTO in carbEntries {
+                    self.storeCarbEntryFromDTO(entryDTO)
+                }
+
+                do {
+                    guard self.context.hasChanges else { return }
+                    try self.context.save()
+                    debugPrint("\(DebuggingIdentifiers.succeeded) Carb history successfully imported into Core Data.")
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to save carb history to Core Data: \(error)")
+                }
+            }
+
+            // Delete JSON
+            try fileManager.removeItem(at: filePath)
+            debugPrint("carbHistory.json deleted after successful import.")
+
+            // Update UserDefaults flag
+            UserDefaults.standard.set(true, forKey: userDefaultsKey)
+        } catch {
+            debugPrint("Error importing carb history: \(error)")
+        }
+    }
+
+    private func storeCarbEntryFromDTO(_ entryDTO: CarbEntryDTO) {
+        let carbEntry = CarbEntryStored(context: context)
+        carbEntry.id = entryDTO.id ?? UUID()
+        carbEntry.carbs = entryDTO.carbs
+        carbEntry.date = entryDTO.date ?? Date()
+        carbEntry.fat = entryDTO.fat ?? 0.0
+        carbEntry.protein = entryDTO.protein ?? 0.0
+        carbEntry.isFPU = entryDTO.isFPU ?? false
+        carbEntry.note = entryDTO.note
+        carbEntry.isUploadedToNS = false
+        carbEntry.isUploadedToHealth = false
+        carbEntry.isUploadedToTidepool = false
+    }
+}

+ 1 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A348" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B83" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>