浏览代码

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Open-iAPS into trio/settings-refactor

Deniz Cengiz 1 年之前
父节点
当前提交
c43c4dff5c

+ 1 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -79,7 +79,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                         glucoseEntry.id = UUID()
                         glucoseEntry.glucose = Int16(entry.glucose ?? 0)
                         glucoseEntry.date = entry.dateString
-                        glucoseEntry.direction = entry.direction?.symbol
+                        glucoseEntry.direction = entry.direction?.rawValue
                         glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
                         debugPrint("\(DebuggingIdentifiers.failed)")
                         debugPrint("\(String(describing: glucoseEntry.direction))")

+ 0 - 2
FreeAPS/Sources/Models/Determination.swift

@@ -19,7 +19,6 @@ struct Determination: JSON, Equatable {
     let reservoir: Decimal?
     let isf: Decimal?
     var timestamp: Date?
-    var recieved: Bool?
     let tdd: Decimal?
     let insulin: Insulin?
     let current_target: Decimal?
@@ -67,7 +66,6 @@ extension Determination {
         case bg
         case reservoir
         case timestamp
-        case recieved
         case isf = "ISF"
         case tdd = "TDD"
         case insulin

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -607,7 +607,7 @@ extension Bolus.StateModel {
     private func setupDeterminationsArray() {
         Task {
             let ids = await determinationStorage.fetchLastDeterminationObjectID(
-                predicate: NSPredicate.enactedDetermination
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
             )
             await updateDeterminationsArray(with: ids)
         }

+ 3 - 4
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -286,9 +286,7 @@ extension Bolus {
                     numberFormatter: mealFormatter
                 )
                 .onChange(of: state.carbs) { _ in
-                    if state.carbs > 0 {
-                        handleDebouncedInput()
-                    }
+                    handleDebouncedInput()
                 }
                 Text("g").foregroundColor(.secondary)
             }
@@ -574,7 +572,8 @@ extension Bolus {
         }
 
         private var disableTaskButton: Bool {
-            state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false
+            state.addButtonPressed ||
+                (state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false)
         }
     }
 

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -248,7 +248,7 @@ extension DataTable {
                             if glucose.isManual {
                                 Image(systemName: "drop.fill").symbolRenderingMode(.monochrome).foregroundStyle(.red)
                             } else {
-                                Text("\(glucose.direction ?? "--")")
+                                Text("\(glucose.directionEnum?.symbol ?? "--")")
                             }
 
                             Spacer()

+ 2 - 2
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -1040,7 +1040,7 @@ extension MainChartView {
     }
 
     private var mainChartXAxis: some AxisContent {
-        AxisMarks(values: .stride(by: .hour, count: screenHours == 24 ? 4 : 2)) { _ in
+        AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
             if displayXgridLines {
                 AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             } else {
@@ -1050,7 +1050,7 @@ extension MainChartView {
     }
 
     private var basalChartXAxis: some AxisContent {
-        AxisMarks(values: .stride(by: .hour, count: screenHours == 24 ? 4 : 2)) { _ in
+        AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
             if displayXgridLines {
                 AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             } else {

+ 166 - 124
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -7,7 +7,7 @@ protocol CalendarManager {
     func requestAccessIfNeeded() async -> Bool
     func calendarIDs() -> [String]
     var currentCalendarID: String? { get set }
-    func createEvent(for glucose: GlucoseStored, delta: Int)
+    func createEvent() async
 }
 
 final class BaseCalendarManager: CalendarManager, Injectable {
@@ -19,13 +19,87 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var storage: FileStorage!
 
+    private var coreDataObserver: CoreDataObserver?
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if settingsManager.settings.units == .mmolL {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.roundingMode = .halfUp
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        formatter.positivePrefix = "+"
+        return formatter
+    }
+
+    private var iobFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    private var cobFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
     init(resolver: Resolver) {
         injectServices(resolver)
-        broadcaster.register(GlucoseObserver.self, observer: self)
-        setupGlucose()
+        setupCurrentCalendar()
+        Task {
+            await createEvent()
+        }
+        coreDataObserver = CoreDataObserver()
+        registerHandlers()
+        setupGlucoseNotification()
     }
 
-    let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
+    let backgroundContext = CoreDataStack.shared.newTaskContext()
+    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
+    private func setupCurrentCalendar() {
+        let calendars = eventStore.calendars(for: .event)
+        if let defaultCalendar = calendars.first {
+            currentCalendarID = defaultCalendar.title
+        }
+    }
+
+    private func registerHandlers() {
+        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+            guard let self = self else { return }
+            Task {
+                await self.createEvent()
+            }
+        }
+    }
+
+    private func setupGlucoseNotification() {
+        /// custom notification that is sent when a batch insert of glucose objects is done
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleBatchInsert),
+            name: .didPerformBatchInsert,
+            object: nil
+        )
+    }
+
+    @objc private func handleBatchInsert() {
+        Task {
+            await createEvent()
+        }
+    }
 
     func requestAccessIfNeeded() async -> Bool {
         let status = EKEventStore.authorizationStatus(for: .event)
@@ -93,92 +167,123 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         EKEventStore().calendars(for: .event).map(\.title)
     }
 
-    private func getLastDetermination() -> [OrefDetermination] {
-        CoreDataStack.shared.fetchEntities(
+    private func getLastDetermination() async -> NSManagedObjectID? {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
-            onContext: coredataContext,
+            onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             key: "timestamp",
             ascending: false,
             fetchLimit: 1,
             propertiesToFetch: ["timestamp", "cob", "iob"]
         )
+        return await backgroundContext.perform {
+            results.first.map(\.objectID)
+        }
+    }
+
+    private func fetchGlucose() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false
+        )
+        return await backgroundContext.perform {
+            return results.map(\.objectID)
+        }
     }
 
-    func createEvent(for glucose: GlucoseStored, delta: Int) {
-        guard settingsManager.settings.useCalendar else { return }
+    @MainActor func createEvent() async {
+        guard settingsManager.settings.useCalendar, let calendar = currentCalendar,
+              let determinationId = await getLastDetermination() else { return }
 
-        guard let calendar = currentCalendar else { return }
+        let glucoseIds = await fetchGlucose()
 
         deleteAllEvents(in: calendar)
 
-        let glucoseValue = glucose.glucose
+        do {
+            guard let determinationObject = try viewContext.existingObject(with: determinationId) as? OrefDetermination
+            else { return }
 
-        // create an event now
-        let event = EKEvent(eventStore: eventStore)
+            let glucoseObjects = try glucoseIds.compactMap { id in
+                try viewContext.existingObject(with: id) as? GlucoseStored
+            }
 
-        // Calendar settings
-        let displeyCOBandIOB = settingsManager.settings.displayCalendarIOBandCOB
-        let displayEmojis = settingsManager.settings.displayCalendarEmojis
+            guard let lastGlucoseObject = glucoseObjects.first, let lastGlucoseValue = glucoseObjects.first?.glucose,
+                  let secondLastReading = glucoseObjects.dropFirst().first?.glucose else { return }
 
-        // Latest Loop data
-        var freshLoop: Double = 20
-        var lastLoop: Date?
-        if displeyCOBandIOB || displayEmojis {
-            lastLoop = getLastDetermination().first?.timestamp
-            freshLoop = -1 * (lastLoop?.timeIntervalSinceNow.minutes ?? 0)
-        }
+            let delta = Decimal(lastGlucoseValue) - Decimal(secondLastReading)
 
-        var glucoseIcon = "🟢"
-        if displayEmojis {
-            glucoseIcon = Double(glucoseValue) <= Double(settingsManager.settings.low) ? "🔴" : glucoseIcon
-            glucoseIcon = Double(glucoseValue) >= Double(settingsManager.settings.high) ? "🟠" : glucoseIcon
-            glucoseIcon = freshLoop > 15 ? "🚫" : glucoseIcon
-        }
+            // create an event now
+            let event = EKEvent(eventStore: eventStore)
+
+            // Calendar settings
+            let displayCOBandIOB = settingsManager.settings.displayCalendarIOBandCOB
+            let displayEmojis = settingsManager.settings.displayCalendarEmojis
+
+            // Latest Loop data
+            var freshLoop: Double = 20
+            var lastLoop: Date?
+            if displayCOBandIOB || displayEmojis {
+                lastLoop = determinationObject.timestamp
+                freshLoop = -1 * (lastLoop?.timeIntervalSinceNow.minutes ?? 0)
+            }
 
-        let glucoseText = glucoseFormatter
-            .string(from: Double(
-                settingsManager.settings.units == .mmolL ? Int(glucoseValue)
-                    .asMmolL : Decimal(glucoseValue)
-            ) as NSNumber)!
+            var glucoseIcon = "🟢"
+            if displayEmojis {
+                glucoseIcon = Double(lastGlucoseValue) <= Double(settingsManager.settings.low) ? "🔴" : glucoseIcon
+                glucoseIcon = Double(lastGlucoseValue) >= Double(settingsManager.settings.high) ? "🟠" : glucoseIcon
+                glucoseIcon = freshLoop > 15 ? "🚫" : glucoseIcon
+            }
 
-        let directionText = glucose.direction ?? "↔︎"
+            let glucoseText = glucoseFormatter
+                .string(from: Double(
+                    settingsManager.settings.units == .mmolL ? Int(lastGlucoseValue)
+                        .asMmolL : Decimal(lastGlucoseValue)
+                ) as NSNumber)!
+            debugPrint("\(DebuggingIdentifiers.failed) glucose text: \(glucoseText)")
 
-        let deltaValue = settingsManager.settings.units == .mmolL ? Int(delta.asMmolL) : delta
-        let deltaText = deltaFormatter.string(from: NSNumber(value: deltaValue)) ?? "--"
+            let directionText = lastGlucoseObject.directionEnum?.symbol ?? "↔︎"
 
-        let iobText = iobFormatter.string(from: (getLastDetermination().first?.iob ?? 0) as NSNumber) ?? ""
-        let cobText = cobFormatter.string(from: (getLastDetermination().first?.cob ?? 0) as NSNumber) ?? ""
+            let deltaValue = settingsManager.settings.units == .mmolL ? Int(delta.asMmolL) : Int(delta)
+            let deltaText = deltaFormatter.string(from: NSNumber(value: deltaValue)) ?? "--"
 
-        var glucoseDisplayText = displayEmojis ? glucoseIcon + " " : ""
-        glucoseDisplayText += glucoseText + " " + directionText + " " + deltaText
+            let iobText = iobFormatter.string(from: (determinationObject.iob ?? 0) as NSNumber) ?? ""
+            let cobText = cobFormatter.string(from: determinationObject.cob as NSNumber) ?? ""
 
-        var iobDisplayText = ""
-        var cobDisplayText = ""
+            var glucoseDisplayText = displayEmojis ? glucoseIcon + " " : ""
+            glucoseDisplayText += glucoseText + " " + directionText + " " + deltaText
 
-        if displeyCOBandIOB {
-            if displayEmojis {
-                iobDisplayText += "💉"
-                cobDisplayText += "🥨"
-            } else {
-                iobDisplayText += "IOB:"
-                cobDisplayText += "COB:"
+            var iobDisplayText = ""
+            var cobDisplayText = ""
+
+            if displayCOBandIOB {
+                if displayEmojis {
+                    iobDisplayText += "💉"
+                    cobDisplayText += "🥨"
+                } else {
+                    iobDisplayText += "IOB:"
+                    cobDisplayText += "COB:"
+                }
+                iobDisplayText += " " + iobText
+                cobDisplayText += " " + cobText
+                event.location = iobDisplayText + " " + cobDisplayText
             }
-            iobDisplayText += " " + iobText
-            cobDisplayText += " " + cobText
-            event.location = iobDisplayText + " " + cobDisplayText
-        }
 
-        event.title = glucoseDisplayText
-        event.notes = "Trio"
-        event.startDate = Date()
-        event.endDate = Date(timeIntervalSinceNow: 60 * 10)
-        event.calendar = calendar
+            event.title = glucoseDisplayText
+            event.notes = "Trio"
+            event.startDate = Date()
+            event.endDate = Date(timeIntervalSinceNow: 60 * 10)
+            event.calendar = calendar
 
-        do {
             try eventStore.save(event, span: .thisEvent)
+
         } catch {
-            warning(.service, "Cannot create calendar event", error: error)
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create calendar event: \(error.localizedDescription)"
+            )
         }
     }
 
@@ -205,69 +310,6 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             }
         }
     }
-
-    private var glucoseFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        if settingsManager.settings.units == .mmolL {
-            formatter.minimumFractionDigits = 1
-            formatter.maximumFractionDigits = 1
-        }
-        formatter.roundingMode = .halfUp
-        return formatter
-    }
-
-    private var deltaFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        formatter.positivePrefix = "+"
-        return formatter
-    }
-
-    private var iobFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        return formatter
-    }
-
-    private var cobFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        return formatter
-    }
-
-    private func setupGlucose() {
-        coredataContext.performAndWait {
-            let results = CoreDataStack.shared.fetchEntities(
-                ofType: GlucoseStored.self,
-                onContext: coredataContext,
-                predicate: NSPredicate.predicateFor30MinAgo,
-                key: "date",
-                ascending: false
-            )
-
-            guard results.count >= 2 else { return }
-
-            if let lastGlucose = results.first,
-               let secondLastReading = results.dropFirst().first?.glucose
-            {
-                let glucoseDelta = lastGlucose.glucose - secondLastReading
-                self.createEvent(for: lastGlucose, delta: Int(glucoseDelta))
-            } else {
-                debugPrint("Failed to unwrap necessary glucose readings")
-            }
-        }
-    }
-}
-
-extension BaseCalendarManager: GlucoseObserver {
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-        setupGlucose()
-    }
 }
 
 extension BloodGlucose.Direction {

+ 62 - 28
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -1,4 +1,5 @@
 import AudioToolbox
+import CoreData
 import Foundation
 import LoopKit
 import SwiftUI
@@ -52,19 +53,24 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private let center = UNUserNotificationCenter.current()
     private var lifetime = Lifetime()
 
-    private let context = CoreDataStack.shared.persistentContainer.viewContext
+    private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    private var coreDataObserver: CoreDataObserver?
 
     init(resolver: Resolver) {
         super.init()
         center.delegate = self
         injectServices(resolver)
-        broadcaster.register(GlucoseObserver.self, observer: self)
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
-
         requestNotificationPermissionsIfNeeded()
-        sendGlucoseNotification()
+        Task {
+            await sendGlucoseNotification()
+        }
+        registerHandlers()
+        setupGlucoseNotification()
         subscribeOnLoop()
     }
 
@@ -76,6 +82,32 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             .store(in: &lifetime)
     }
 
+    private func registerHandlers() {
+        // Due to the Batch insert this only is used for observing Deletion of Glucose entries
+        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+            guard let self = self else { return }
+            Task {
+                await self.sendGlucoseNotification()
+            }
+        }
+    }
+
+    private func setupGlucoseNotification() {
+        /// custom notification that is sent when a batch insert of glucose objects is done
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleBatchInsert),
+            name: .didPerformBatchInsert,
+            object: nil
+        )
+    }
+
+    @objc private func handleBatchInsert() {
+        Task {
+            await sendGlucoseNotification()
+        }
+    }
+
     private func addAppBadge(glucose: Int?) {
         guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
             DispatchQueue.main.async {
@@ -184,32 +216,37 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         }
     }
 
-    private func fetchGlucose() -> [GlucoseStored]? {
-        CoreDataStack.shared.fetchEntities(
+    private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: context,
+            onContext: backgroundContext,
             predicate: NSPredicate.predicateFor20MinAgo,
             key: "date",
-            ascending: true,
+            ascending: false,
             fetchLimit: 3
         )
+        return await backgroundContext.perform {
+            return results.map(\.objectID)
+        }
     }
 
-    private func sendGlucoseNotification() {
-        addAppBadge(glucose: nil)
+    @MainActor private func sendGlucoseNotification() async {
+        do {
+            addAppBadge(glucose: nil)
+            let glucoseIDs = await fetchGlucoseIDs()
+            let glucoseObjects = try glucoseIDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? GlucoseStored
+            }
 
-        context.perform {
-            guard let glucose = self.fetchGlucose(), let lastValue = glucose.first, let lastReading = glucose.first?.glucose,
-                  let lastDirection = lastValue.direction,
-                  let secondLastReading = glucose.dropFirst().first?.glucose else { return }
+            guard let lastReading = glucoseObjects.first?.glucose,
+                  let secondLastReading = glucoseObjects.dropFirst().first?.glucose,
+                  let lastDirection = glucoseObjects.first?.direction else { return }
 
-            self.addAppBadge(glucose: (glucose.first?.glucose).map { Int($0) })
+            addAppBadge(glucose: (glucoseObjects.first?.glucose).map { Int($0) })
 
-            guard self.glucoseStorage.alarm != nil || self.settingsManager.settings.glucoseNotificationsAlways else {
-                return
-            }
+            guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
 
-            self.ensureCanSendNotification {
+            ensureCanSendNotification {
                 var titles: [String] = []
                 var notificationAlarm = false
 
@@ -224,13 +261,12 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                     notificationAlarm = true
                 }
 
-                let delta = glucose.count >= 2 ? lastReading - secondLastReading : nil
+                let delta = glucoseObjects.count >= 2 ? lastReading - secondLastReading : nil
                 let body = self.glucoseText(
                     glucoseValue: Int(lastReading),
                     delta: Int(delta ?? 0),
                     direction: lastDirection
-                ) + self
-                    .infoBody()
+                ) + self.infoBody()
 
                 if self.snoozeUntilDate > Date() {
                     titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
@@ -250,6 +286,10 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                     self.addRequest(identifier: .glucocoseNotification, content: content, deleteOld: true)
                 }
             }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to send glucose notification with error: \(error.localizedDescription)"
+            )
         }
     }
 
@@ -414,12 +454,6 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     }
 }
 
-extension BaseUserNotificationsManager: GlucoseObserver {
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-        sendGlucoseNotification()
-    }
-}
-
 extension BaseUserNotificationsManager: pumpNotificationObserver {
     func pumpNotification(alert: AlertEntry) {
         ensureCanSendNotification {