Browse Source

Override to NS WIP

Deniz Cengiz 1 năm trước cách đây
mục cha
commit
e820007461

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -447,6 +447,7 @@
 		D8DA5CD12B49EEF000C54E6C /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = D8DA5CD02B49EEF000C54E6C /* SlideButton */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
 		DD399FB31EACB9343C944C4C /* PreferencesEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3E609094E064C99A4752C /* PreferencesEditorStateModel.swift */; };
+		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFD27368630002FF094 /* ServiceAssembly.swift */; };
 		E00EEC0427368630002FF094 /* SecurityAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFE27368630002FF094 /* SecurityAssembly.swift */; };
 		E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFF27368630002FF094 /* StorageAssembly.swift */; };
@@ -1046,6 +1047,7 @@
 		D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorProvider.swift; sourceTree = "<group>"; };
 		DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		DC2C6489D29ECCCAD78E0721 /* NotificationsConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigStateModel.swift; sourceTree = "<group>"; };
+		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		E00EEBFD27368630002FF094 /* ServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFE27368630002FF094 /* SecurityAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFF27368630002FF094 /* StorageAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageAssembly.swift; sourceTree = "<group>"; };
@@ -1800,6 +1802,7 @@
 				383948D925CD64D500E91849 /* Glucose.swift */,
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 				38887CCD25F5725200944304 /* IOBEntry.swift */,
+				DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */,
 				385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */,
 				389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */,
 				3895E4C525B9E00D00214B37 /* Preferences.swift */,
@@ -3134,6 +3137,7 @@
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
+				DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */,
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				FEFA5C0F299F810B00765C17 /* Core_Data.xcdatamodeld in Sources */,

+ 53 - 0
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -9,6 +9,8 @@ protocol OverrideStorage {
     func storeOverride(override: Override) async
     func copyRunningOverride(_ override: OverrideStored) async
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
+    func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
+    func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
 }
 
 final class BaseOverrideStorage: OverrideStorage, Injectable {
@@ -84,6 +86,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             newOverride.id = UUID().uuidString
             newOverride.date = override.date
             newOverride.isPreset = override.isPreset
+            newOverride.isUploadedToNS = false
 
             // Assign orderPosition if it's a preset and presetCount is valid
             if override.isPreset, presetCount > -1 {
@@ -177,4 +180,54 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
     @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
     }
+
+    func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise] {
+        let fetchedOverrides = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout,
+            key: "date",
+            ascending: false
+        )
+
+        return await backgroundContext.perform {
+            return fetchedOverrides.map { override in
+                let duration = override.indefinite ? 86400 : override.duration ?? 0 // 86400 min = 1 day
+                return NightscoutExercise(
+                    duration: Int(truncating: duration),
+                    eventType: OverrideStored.EventType.nsExercise,
+                    createdAt: override.date ?? Date(),
+                    enteredBy: NightscoutExercise.local,
+                    notes: override.name ?? "Custom Override"
+                )
+            }
+        }
+    }
+
+    func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise] {
+        let fetchedOverrideRuns = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideRunStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate(
+                format: "startDate >= %@ AND isUploadedToNS == %@",
+                Date.oneDayAgo as NSDate,
+                false as NSNumber
+            ),
+            key: "startDate",
+            ascending: false
+        )
+
+        return await backgroundContext.perform {
+            return fetchedOverrideRuns.map { overrideRun in
+                let durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 0) / 60
+                return NightscoutExercise(
+                    duration: Int(durationInMinutes),
+                    eventType: OverrideStored.EventType.nsExercise,
+                    createdAt: (overrideRun.startDate ?? overrideRun.override?.date) ?? Date(),
+                    enteredBy: NightscoutExercise.local,
+                    notes: overrideRun.override?.name ?? "Custom Override"
+                )
+            }
+        }
+    }
 }

+ 29 - 0
FreeAPS/Sources/Models/NightscoutExercise.swift

@@ -0,0 +1,29 @@
+import Foundation
+
+struct NightscoutExercise: JSON, Hashable, Equatable {
+    var duration: Int?
+    var eventType: OverrideStored.EventType
+    var createdAt: Date
+    var enteredBy: String?
+    var notes: String?
+
+    static let local = "Trio"
+
+    static func == (lhs: NightscoutExercise, rhs: NightscoutExercise) -> Bool {
+        (lhs.createdAt) == rhs.createdAt
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(createdAt)
+    }
+}
+
+extension NightscoutExercise {
+    private enum CodingKeys: String, CodingKey {
+        case duration
+        case eventType
+        case createdAt = "created_at"
+        case enteredBy
+        case notes
+    }
+}

+ 1 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -921,6 +921,7 @@ extension Home.StateModel {
                 newOverrideRunStored.endDate = Date()
                 newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
                 newOverrideRunStored.override = object
+                newOverrideRunStored.isUploadedToNS = false
 
                 guard self.viewContext.hasChanges else { return }
                 try self.viewContext.save()

+ 1 - 0
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -210,6 +210,7 @@ extension OverrideProfilesConfig.StateModel {
                         newOverrideRunStored
                             .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
                         newOverrideRunStored.override = canceledOverride
+                        newOverrideRunStored.isUploadedToNS = false
                     }
                 }
 

+ 1 - 1
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/AddOverrideForm.swift

@@ -261,7 +261,7 @@ struct AddOverrideForm: View {
                     }
                 }
             }
-            label: { Text("Save as Override") }
+            label: { Text("Save as Preset") }
                 .tint(.orange)
                 .frame(maxWidth: .infinity, alignment: .trailing)
                 .buttonStyle(BorderlessButtonStyle())

+ 36 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -560,6 +560,42 @@ extension NightscoutAPI {
             .map { _ in () }
             .eraseToAnyPublisher()
     }
+
+    func uploadOverrides(_ overrides: [NightscoutExercise]) async throws {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+        do {
+            let encodedBody = try JSONCoding.encoder.encode(overrides)
+            request.httpBody = encodedBody
+            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
+            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
+        } catch {
+            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            throw error
+        }
+        request.httpMethod = "POST"
+
+        let (data, response) = try await URLSession.shared.data(for: request)
+
+        // Check the response status code
+        guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
+            throw URLError(.badServerResponse)
+        }
+
+        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
+    }
 }
 
 private extension String {

+ 98 - 0
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -26,6 +26,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var keychain: Keychain!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
+    @Injected() private var overridesStorage: OverrideStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var storage: FileStorage!
@@ -808,6 +809,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
     }
 
+    private func uploadOverrides() async {
+        await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
+        await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
+    }
+
     private func uploadTempTargets() async {
         await uploadTreatments(
             tempTargetsStorage.nightscoutTreatmentsNotUploaded(),
@@ -987,6 +993,92 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
         }
     }
+
+    private func uploadOverrides(_ overrides: [NightscoutExercise]) async {
+        guard !overrides.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
+            return
+        }
+
+        do {
+            for chunk in overrides.chunks(ofCount: 100) {
+                try await nightscout.uploadOverrides(Array(chunk))
+            }
+
+            // If successful, update the isUploadedToNS property of the OverrideStored objects
+            await updateOverridesAsUploaded(overrides)
+
+            debug(.nightscout, "Overrides uploaded")
+        } catch {
+            debug(.nightscout, error.localizedDescription)
+        }
+    }
+
+    private func updateOverridesAsUploaded(_ overrides: [NightscoutExercise]) async {
+        await backgroundContext.perform {
+            let ids = overrides.map(\.id) as NSArray
+            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
+            let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
+                for result in results {
+                    result.isUploadedToNS = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    private func uploadOverrideRuns(_ overrideRuns: [NightscoutExercise]) async {
+        guard !overrideRuns.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
+            return
+        }
+
+        do {
+            for chunk in overrideRuns.chunks(ofCount: 100) {
+                try await nightscout.uploadOverrides(Array(chunk))
+            }
+
+            // If successful, update the isUploadedToNS property of the OverrideRunStored objects
+            await updateOverrideRunsAsUploaded(overrideRuns)
+
+            debug(.nightscout, "Overrides uploaded")
+        } catch {
+            debug(.nightscout, error.localizedDescription)
+        }
+    }
+
+    private func updateOverrideRunsAsUploaded(_ overrideRuns: [NightscoutExercise]) async {
+        await backgroundContext.perform {
+            let ids = overrideRuns.map(\.id) as NSArray
+            print("\(DebuggingIdentifiers.inProgress) ids: \(ids)")
+            let fetchRequest: NSFetchRequest<OverrideRunStored> = OverrideRunStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                print("\(DebuggingIdentifiers.inProgress) results: \(results)")
+                for result in results {
+                    result.isUploadedToNS = true
+                }
+
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
+                )
+            }
+        }
+    }
 }
 
 extension Array {
@@ -1029,6 +1121,7 @@ extension BaseNightscoutManager {
         let manualGlucoseUpdates = objects.filter { $0 is GlucoseStored }
         let carbUpdates = objects.filter { $0 is CarbEntryStored }
         let pumpHistoryUpdates = objects.filter { $0 is PumpEventStored }
+        let overrideUpdates = objects.filter { $0 is OverrideStored || $0 is OverrideRunStored }
 
         if manualGlucoseUpdates.isNotEmpty {
             Task.detached {
@@ -1045,5 +1138,10 @@ extension BaseNightscoutManager {
                 await self.uploadPumpHistory()
             }
         }
+        if overrideUpdates.isNotEmpty {
+            Task.detached {
+                await self.uploadOverrides()
+            }
+        }
     }
 }

+ 2 - 0
Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -123,6 +123,7 @@
     <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
         <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="startDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="target" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <relationship name="override" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="OverrideStored" inverseName="overrideRun" inverseEntity="OverrideStored"/>
@@ -142,6 +143,7 @@
         <attribute name="isf" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
         <attribute name="isfAndCr" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isPreset" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
+        <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="name" optional="YES" attributeType="String"/>
         <attribute name="orderPosition" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="percentage" optional="YES" attributeType="Double" defaultValueString="100" usesScalarValueType="YES"/>

+ 16 - 0
Model/Helper/OverrideStored+helper.swift

@@ -14,6 +14,16 @@ extension NSPredicate {
             true as NSNumber
         )
     }
+
+    static var lastActiveOverrideNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND enabled == %@ AND isUploadedToNS == %@",
+            date as NSDate,
+            true as NSNumber,
+            false as NSNumber
+        )
+    }
 }
 
 extension OverrideStored {
@@ -27,3 +37,9 @@ extension OverrideStored {
         return request
     }
 }
+
+extension OverrideStored {
+    enum EventType: String, JSON {
+        case nsExercise = "Exercise"
+    }
+}

+ 1 - 0
Model/Helper/PumpEvent+helper.swift

@@ -42,6 +42,7 @@ public extension PumpEventStored {
         case nsBatteryChange = "Pump Battery Change"
         case nsAnnouncement = "Announcement"
         case nsSensorChange = "Sensor Start"
+        case nsExercise = "Exercise"
         case capillaryGlucose = "BG Check"
     }
 

+ 2 - 1
OverrideRunStored+CoreDataProperties.swift

@@ -7,9 +7,10 @@ public extension OverrideRunStored {
     }
 
     @NSManaged var endDate: Date?
+    @NSManaged var id: UUID?
     @NSManaged var startDate: Date?
     @NSManaged var target: NSDecimalNumber?
-    @NSManaged var id: UUID?
+    @NSManaged var isUploadedToNS: Bool
     @NSManaged var override: OverrideStored?
 }
 

+ 2 - 1
OverrideStored+CoreDataProperties.swift

@@ -18,6 +18,7 @@ public extension OverrideStored {
     @NSManaged var isfAndCr: Bool
     @NSManaged var isPreset: Bool
     @NSManaged var name: String?
+    @NSManaged var orderPosition: Int16
     @NSManaged var percentage: Double
     @NSManaged var smbIsAlwaysOff: Bool
     @NSManaged var smbIsOff: Bool
@@ -25,7 +26,7 @@ public extension OverrideStored {
     @NSManaged var start: NSDecimalNumber?
     @NSManaged var target: NSDecimalNumber?
     @NSManaged var uamMinutes: NSDecimalNumber?
-    @NSManaged var orderPosition: Int16
+    @NSManaged var isUploadedToNS: Bool
     @NSManaged var overrideRun: OverrideRunStored?
 }