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

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

Deniz Cengiz 1 год назад
Родитель
Сommit
b791c5c23a

+ 24 - 4
FreeAPS/Sources/APS/CGM/PluginSource.swift

@@ -1,5 +1,7 @@
+import CGMBLEKit
 import Combine
 import Foundation
+import G7SensorKit
 import LibreTransmitter
 import LoopKit
 import LoopKitUI
@@ -162,11 +164,29 @@ extension PluginSource: CGMManagerDelegate {
         case let .newData(values):
 
             var sensorActivatedAt: Date?
+            var sensorStartDate: Date?
             var sensorTransmitterID: String?
-            /// specific for Libre transmitter and send SAGE
+
+            /// SAGE
             if let cgmTransmitterManager = cgmManager as? LibreTransmitterManagerV3 {
-                sensorActivatedAt = cgmTransmitterManager.sensorInfoObservable.activatedAt
-                sensorTransmitterID = cgmTransmitterManager.sensorInfoObservable.sensorSerial
+                let sensorInfo = cgmTransmitterManager.sensorInfoObservable
+                sensorActivatedAt = sensorInfo.activatedAt
+                sensorStartDate = sensorInfo.activatedAt
+                sensorTransmitterID = sensorInfo.sensorSerial
+            } else if let cgmTransmitterManager = cgmManager as? G5CGMManager {
+                let latestReading = cgmTransmitterManager.latestReading
+                sensorActivatedAt = latestReading?.activationDate
+                sensorStartDate = latestReading?.sessionStartDate
+                sensorTransmitterID = latestReading?.transmitterID
+            } else if let cgmTransmitterManager = cgmManager as? G6CGMManager {
+                let latestReading = cgmTransmitterManager.latestReading
+                sensorActivatedAt = latestReading?.activationDate
+                sensorStartDate = latestReading?.sessionStartDate
+                sensorTransmitterID = latestReading?.transmitterID
+            } else if let cgmTransmitterManager = cgmManager as? G7CGMManager {
+                sensorActivatedAt = cgmTransmitterManager.sensorActivatedAt
+                sensorStartDate = cgmTransmitterManager.sensorActivatedAt
+                sensorTransmitterID = cgmTransmitterManager.sensorName
             }
 
             let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
@@ -185,7 +205,7 @@ extension PluginSource: CGMManagerDelegate {
                     glucose: value,
                     type: "sgv",
                     activationDate: sensorActivatedAt,
-                    sessionStartDate: sensorActivatedAt,
+                    sessionStartDate: sensorStartDate,
                     transmitterID: sensorTransmitterID
                 )
             }

+ 1 - 1
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -367,7 +367,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
         if case .inProgress = status.bolusState {
             bolusTrigger.send(true)
-        } else if status.bolusState != .canceling {
+        } else {
             bolusTrigger.send(false)
         }
 

+ 20 - 10
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -19,34 +19,44 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
 
         init?(from string: String) {
             switch string {
-            case "↑↑↑",
+            case "\u{2191}\u{2191}\u{2191}",
+                 "↑↑↑",
                  "TripleUp":
                 self = .tripleUp
-            case "↑↑",
+            case "\u{2191}\u{2191}",
+                 "↑↑",
                  "DoubleUp":
                 self = .doubleUp
-            case "↑",
+            case "\u{2191}",
+                 "↑",
                  "SingleUp":
                 self = .singleUp
-            case "↗︎",
+            case "\u{2197}",
+                 "↗︎",
                  "FortyFiveUp":
                 self = .fortyFiveUp
-            case "→",
+            case "\u{2192}",
+                 "→",
                  "Flat":
                 self = .flat
-            case "↘︎",
+            case "\u{2198}",
+                 "↘︎",
                  "FortyFiveDown":
                 self = .fortyFiveDown
-            case "↓",
+            case "\u{2193}",
+                 "↓",
                  "SingleDown":
                 self = .singleDown
-            case "↓↓",
+            case "\u{2193}\u{2193}",
+                 "↓↓",
                  "DoubleDown":
                 self = .doubleDown
-            case "↓↓↓",
+            case "\u{2193}\u{2193}\u{2193}",
+                 "↓↓↓",
                  "TripleDown":
                 self = .tripleDown
-            case "↔︎",
+            case "\u{2194}",
+                 "↔︎",
                  "NONE":
                 self = .none
             case "NOT COMPUTABLE":

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

@@ -67,13 +67,12 @@ extension DataTable {
         private var manualGlucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
             if state.units == .mmolL {
+                formatter.minimumFractionDigits = 0
                 formatter.maximumFractionDigits = 1
-                formatter.minimumFractionDigits = 1
-                formatter.roundingMode = .ceiling
-            } else {
-                formatter.maximumFractionDigits = 0
             }
+            formatter.roundingMode = .down
             return formatter
         }
 
@@ -331,6 +330,7 @@ extension DataTable {
                                 TextFieldWithToolBar(
                                     text: $state.manualGlucose,
                                     placeholder: " ... ",
+                                    shouldBecomeFirstResponder: true,
                                     numberFormatter: manualGlucoseFormatter
                                 )
                                 Text(state.units.rawValue).foregroundStyle(.secondary)

+ 7 - 3
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -52,6 +52,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         settingsManager.settings.isUploadEnabled
     }
 
+    private var isDownloadEnabled: Bool {
+        settingsManager.settings.isDownloadEnabled
+    }
+
     private var isUploadGlucoseEnabled: Bool {
         settingsManager.settings.uploadGlucose
     }
@@ -196,7 +200,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func fetchCarbs() async -> [CarbsEntry] {
-        guard let nightscout = nightscoutAPI, isNetworkReachable else {
+        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
             return []
         }
 
@@ -211,7 +215,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func fetchTempTargets() async -> [TempTarget] {
-        guard let nightscout = nightscoutAPI, isNetworkReachable else {
+        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
             return []
         }
 
@@ -226,7 +230,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never> {
-        guard let nightscout = nightscoutAPI, isNetworkReachable else {
+        guard let nightscout = nightscoutAPI, isNetworkReachable, isDownloadEnabled else {
             return Just([]).eraseToAnyPublisher()
         }
 

+ 226 - 200
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -18,7 +18,45 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var garmin: GarminManager!
 
+    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 eventualFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = settingsManager.settings.units == .mmolL ? 1 : 0
+        formatter.positivePrefix = "+"
+        formatter.negativePrefix = "-"
+        return formatter
+    }
+
+    private var targetFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
     let context = CoreDataStack.shared.newTaskContext()
+    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
+    private var coreDataObserver: CoreDataObserver?
 
     private var lifetime = Lifetime()
 
@@ -26,13 +64,18 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         self.session = session
         super.init()
         injectServices(resolver)
+        setupNotification()
+        coreDataObserver = CoreDataObserver()
+        registerHandlers()
+        Task {
+            await configureState()
+        }
 
         if WCSession.isSupported() {
             session.delegate = self
             session.activate()
         }
 
-        broadcaster.register(GlucoseObserver.self, observer: self)
         broadcaster.register(SettingsObserver.self, observer: self)
         broadcaster.register(PumpHistoryObserver.self, observer: self)
         broadcaster.register(PumpSettingsObserver.self, observer: self)
@@ -49,179 +92,208 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             return data
         }
 
-        configureState()
+        Task {
+            await configureState()
+        }
     }
 
-    private func fetchlastDetermination() -> [OrefDetermination]? {
-        let predicate = NSPredicate.enactedDetermination
+    func setupNotification() {
+        /// 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
+        )
+    }
 
-        return CoreDataStack.shared.fetchEntities(
+    @objc private func handleBatchInsert() {
+        Task {
+            await self.configureState()
+        }
+    }
+
+    private func registerHandlers() {
+        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+            guard let self = self else { return }
+            Task {
+                await self.configureState()
+            }
+        }
+        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+            guard let self = self else { return }
+            Task {
+                await self.configureState()
+            }
+        }
+        // Observes Deletion of Glucose Objects
+        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+            guard let self = self else { return }
+            Task {
+                await self.configureState()
+            }
+        }
+    }
+
+    private func fetchlastDetermination() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
-            predicate: predicate,
+            predicate: NSPredicate.enactedDetermination,
             key: "timestamp",
             ascending: false,
             fetchLimit: 1,
             propertiesToFetch: ["timestamp"]
         )
+
+        return await context.perform {
+            results.map(\.objectID)
+        }
     }
 
-    private func fetchLatestOverride() -> OverrideStored? {
-        CoreDataStack.shared.fetchEntities(
+    private func fetchLatestOverride() async -> NSManagedObjectID? {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             key: "date",
             ascending: false,
             fetchLimit: 1
-        ).first
-    }
-
-    func fetchAndProcessGlucose() -> (ids: [NSManagedObjectID], glucose: String, trend: String, delta: String, date: Date) {
-        var results: (ids: [NSManagedObjectID], glucose: String, trend: String, delta: String, date: Date) = (
-            [],
-            "--",
-            "--",
-            "--",
-            Date()
         )
 
-        context.perform {
-            let predicate = NSPredicate.predicateFor120MinAgo
-            let fetchedGlucose = CoreDataStack.shared.fetchEntities(
-                ofType: GlucoseStored.self,
-                onContext: self.context,
-                predicate: predicate,
-                key: "date",
-                ascending: false,
-                fetchLimit: 24,
-                batchSize: 12
-            )
-
-            let ids = fetchedGlucose.map(\.objectID)
-            guard let firstGlucose = fetchedGlucose.first else {
-                return
-            }
-
-            let glucoseValue = firstGlucose.glucose
-            let date = firstGlucose.date ?? .distantPast
-            let delta = fetchedGlucose.count >= 2 ? glucoseValue - fetchedGlucose[1].glucose : 0
+        return await context.perform {
+            results.map(\.objectID).first
+        }
+    }
 
-            let units = self.settingsManager.settings.units
-            let glucoseFormatter = NumberFormatter()
-            glucoseFormatter.numberStyle = .decimal
-            glucoseFormatter.maximumFractionDigits = (units == .mmolL) ? 1 : 0
+    private func fetchGlucose() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateFor120MinAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 24,
+            batchSize: 12
+        )
 
-            let glucoseText = glucoseFormatter
-                .string(from: Double(units == .mmolL ? Decimal(glucoseValue).asMmolL : Decimal(glucoseValue)) as NSNumber) ?? "--"
+        return await context.perform {
+            results.map(\.objectID)
+        }
+    }
 
-            let directionText = firstGlucose.direction ?? "↔︎"
+    @MainActor private func configureState() async {
+        let glucoseValuesIDs = await fetchGlucose()
+        guard let lastDeterminationID = await fetchlastDetermination().first,
+              let latestOverrideID = await fetchLatestOverride() else { return }
 
-            let deltaFormatter = NumberFormatter()
-            deltaFormatter.numberStyle = .decimal
-            deltaFormatter.maximumFractionDigits = 1
-            let deltaText = deltaFormatter
-                .string(from: Double(units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)) as NSNumber) ?? "--"
+        do {
+            let glucoseValues = try glucoseValuesIDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? GlucoseStored
+            }
 
-            results = (ids, glucoseText, directionText, deltaText, date)
-        }
-        return results
-    }
+            let lastDetermination = try viewContext.existingObject(with: lastDeterminationID) as? OrefDetermination
+            let latestOverride = try viewContext.existingObject(with: latestOverrideID) as? OverrideStored
+
+            if let firstGlucoseValue = glucoseValues.first {
+                let value = settingsManager.settings
+                    .units == .mgdL ? Decimal(firstGlucoseValue.glucose) : Decimal(firstGlucoseValue.glucose).asMmolL
+                state.glucose = glucoseFormatter.string(from: value as NSNumber)
+                state.trend = firstGlucoseValue.direction
+                let delta = glucoseValues
+                    .count >= 2 ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseValues.dropFirst().first?.glucose ?? 0) : 0
+                let deltaConverted = settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
+                state.delta = deltaFormatter.string(from: deltaConverted as NSNumber)
+                state.trendRaw = firstGlucoseValue.direction
+                state.glucoseDate = firstGlucoseValue.date
+            }
 
-    private func configureState() {
-        processQueue.async {
-            self.context.performAndWait {
-                let glucoseValues = self.fetchAndProcessGlucose()
-                let lastDetermination = self.fetchlastDetermination()?.first
-
-                self.state.glucose = glucoseValues.glucose
-                self.state.trend = glucoseValues.trend
-                self.state.delta = glucoseValues.delta
-                self.state.trendRaw = glucoseValues.trend
-                self.state.glucoseDate = glucoseValues.date
-                self.state.lastLoopDate = lastDetermination?.timestamp
-                self.state.lastLoopDateInterval = self.state.lastLoopDate.map {
-                    guard $0.timeIntervalSince1970 > 0 else { return 0 }
-                    return UInt64($0.timeIntervalSince1970)
-                }
-                self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement
-                self.state.maxCOB = self.settingsManager.preferences.maxCOB
-                self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
-                self.state.carbsRequired = lastDetermination?.carbsRequired as? Decimal
+            state.lastLoopDate = lastDetermination?.timestamp
+            state.lastLoopDateInterval = state.lastLoopDate.map {
+                guard $0.timeIntervalSince1970 > 0 else { return 0 }
+                return UInt64($0.timeIntervalSince1970)
+            }
+            state.bolusIncrement = settingsManager.preferences.bolusIncrement
+            state.maxCOB = settingsManager.preferences.maxCOB
+            state.maxBolus = settingsManager.pumpSettings.maxBolus
+            state.carbsRequired = lastDetermination?.carbsRequired as? Decimal
 
-                var insulinRequired = lastDetermination?.insulinReq as? Decimal ?? 0
+            var insulinRequired = lastDetermination?.insulinReq as? Decimal ?? 0
 
-                var double: Decimal = 2
-                if lastDetermination?.manualBolusErrorString == 0 {
-                    insulinRequired = lastDetermination?.insulinForManualBolus as? Decimal ?? 0
-                    double = 1
-                }
+            var double: Decimal = 2
+            if lastDetermination?.manualBolusErrorString == 0 {
+                insulinRequired = lastDetermination?.insulinForManualBolus as? Decimal ?? 0
+                double = 1
+            }
 
-                self.state.useNewCalc = self.settingsManager.settings.useCalc
+            state.useNewCalc = settingsManager.settings.useCalc
 
-                if !(self.state.useNewCalc ?? false) {
-                    self.state.bolusRecommended = self.apsManager
-                        .roundBolus(amount: max(
-                            insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * double,
-                            0
-                        ))
-                } else {
-                    let recommended = self.newBolusCalc(
-                        ids: glucoseValues.ids,
-                        determination: lastDetermination
+            if !(state.useNewCalc ?? false) {
+                state.bolusRecommended = apsManager
+                    .roundBolus(amount: max(
+                        insulinRequired * (settingsManager.settings.insulinReqPercentage / 100) * double,
+                        0
+                    ))
+            } else {
+                let recommended = await newBolusCalc(
+                    ids: glucoseValuesIDs,
+                    determination: lastDetermination
+                )
+                state.bolusRecommended = apsManager
+                    .roundBolus(amount: max(recommended, 0))
+            }
+            state.bolusAfterCarbs = !settingsManager.settings.skipBolusScreenAfterCarbs
+            state.displayOnWatch = settingsManager.settings.displayOnWatch
+            state.displayFatAndProteinOnWatch = settingsManager.settings.displayFatAndProteinOnWatch
+            state.confirmBolusFaster = settingsManager.settings.confirmBolusFaster
+
+            state.iob = lastDetermination?.iob as? Decimal
+            state.cob = lastDetermination?.cob as? Decimal
+            state.tempTargets = tempTargetsStorage.presets()
+                .map { target -> TempTargetWatchPreset in
+                    let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in
+                        guard currentTarget.id == target.id else { return nil }
+                        let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60))
+                        return date > Date() ? date : nil
+                    }
+                    return TempTargetWatchPreset(
+                        name: target.displayName,
+                        id: target.id,
+                        description: self.descriptionForTarget(target),
+                        until: untilDate
                     )
-                    self.state.bolusRecommended = self.apsManager
-                        .roundBolus(amount: max(recommended, 0))
                 }
-                self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
-                self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
-                self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
-                self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
-
-                self.state.iob = lastDetermination?.iob as? Decimal
-                self.state.cob = lastDetermination?.cob as? Decimal
-                self.state.tempTargets = self.tempTargetsStorage.presets()
-                    .map { target -> TempTargetWatchPreset in
-                        let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in
-                            guard currentTarget.id == target.id else { return nil }
-                            let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60))
-                            return date > Date() ? date : nil
-                        }
-                        return TempTargetWatchPreset(
-                            name: target.displayName,
-                            id: target.id,
-                            description: self.descriptionForTarget(target),
-                            until: untilDate
-                        )
-                    }
-                self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
-                self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
-                self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
-                self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
-
-                let eBG = self.evetualBGStraing()
-                self.state.eventualBG = eBG.map { "⇢ " + $0 }
-                self.state.eventualBGRaw = eBG
-
-                self.state.isf = lastDetermination?.insulinSensitivity as? Decimal
+            state.bolusAfterCarbs = !settingsManager.settings.skipBolusScreenAfterCarbs
+            state.displayOnWatch = settingsManager.settings.displayOnWatch
+            state.displayFatAndProteinOnWatch = settingsManager.settings.displayFatAndProteinOnWatch
+            state.confirmBolusFaster = settingsManager.settings.confirmBolusFaster
+
+            if let eventualBG = settingsManager.settings.units == .mgdL ? lastDetermination?.eventualBG : lastDetermination?
+                .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
+            {
+                let eventualBGAsString = eventualFormatter.string(from: eventualBG)
+                state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
+                state.eventualBGRaw = eventualBGAsString
+            }
 
-                let latestOverride = self.fetchLatestOverride()
+            state.isf = lastDetermination?.insulinSensitivity as? Decimal
 
-                if latestOverride?.enabled ?? false {
-                    let percentString = "\((latestOverride?.percentage ?? 100).formatted(.number)) %"
-                    self.state.override = percentString
+            if latestOverride?.enabled ?? false {
+                let percentString = "\((latestOverride?.percentage ?? 100).formatted(.number)) %"
+                state.override = percentString
 
-                } else {
-                    self.state.override = "100 %"
-                }
+            } else {
+                state.override = "100 %"
             }
 
-            self.sendState()
+            sendState()
+
+        } catch let error as NSError {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to configure state with error: \(error)")
         }
     }
 
     private func sendState() {
-        dispatchPrecondition(condition: .onQueue(processQueue))
         guard let data = try? JSONEncoder().encode(state) else {
             warning(.service, "Cannot encode watch state")
             return
@@ -252,25 +324,11 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         return description
     }
 
-    private func evetualBGStraing() -> String? {
-        context.perform {
-            guard let eventualBG = self.fetchlastDetermination()?.first?.eventualBG as? Int else {
-                return nil
-            }
-            let units = self.settingsManager.settings.units
-            return eventualFormatter.string(
-                from: (units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber
-            )!
-        }
-    }
-
-    private func newBolusCalc(ids: [NSManagedObjectID], determination: OrefDetermination?) -> Decimal {
-        var insulinCalculated: Decimal = 0
-
-        context.performAndWait {
+    private func newBolusCalc(ids: [NSManagedObjectID], determination: OrefDetermination?) async -> Decimal {
+        await context.perform {
             let glucoseObjects = ids.compactMap { self.context.object(with: $0) as? GlucoseStored }
             guard let firstGlucose = glucoseObjects.first else {
-                return // If there's no glucose data, exit the block
+                return 0 // If there's no glucose data, exit the block
             }
             let bg = firstGlucose.glucose // Make sure to provide a fallback value for glucose
 
@@ -280,7 +338,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
                 bgDelta = Int(firstGlucose.glucose) - Int(glucoseObjects[2].glucose)
             }
 
-            let conversion: Decimal = settingsManager.settings.units == .mmolL ? 0.0555 : 1
+            let conversion: Decimal = self.settingsManager.settings.units == .mmolL ? 0.0555 : 1
             let isf = self.state.isf ?? 0
             let target = determination?.currentTarget as? Decimal ?? 100
             let carbratio = determination?.carbRatio as? Decimal ?? 10
@@ -296,51 +354,18 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             let iobInsulinReduction = -iob
             let wholeCalc = targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin
 
-            let result = wholeCalc * settingsManager.settings.overrideFactor
-            if settingsManager.settings.fattyMeals {
+            let result = wholeCalc * self.settingsManager.settings.overrideFactor
+            var insulinCalculated: Decimal
+            if self.settingsManager.settings.fattyMeals {
                 insulinCalculated = result * fattyMealFactor
             } else {
                 insulinCalculated = result
             }
-        }
-
-        // Ensure the calculated insulin amount does not exceed the maximum bolus and is not below zero
-        insulinCalculated = max(min(insulinCalculated, settingsManager.pumpSettings.maxBolus), 0)
-        return insulinCalculated // Return the calculated insulin outside of the performAndWait block
-    }
 
-    private var glucoseFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        if settingsManager.settings.units == .mmolL {
-            formatter.minimumFractionDigits = 1
-            formatter.maximumFractionDigits = 1
+            // Ensure the calculated insulin amount does not exceed the maximum bolus and is not below zero
+            insulinCalculated = max(min(insulinCalculated, self.settingsManager.pumpSettings.maxBolus), 0)
+            return insulinCalculated // Return the calculated insulin outside of the performAndWait block
         }
-        formatter.roundingMode = .halfUp
-        return formatter
-    }
-
-    private var eventualFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        return formatter
-    }
-
-    private var deltaFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        formatter.positivePrefix = "+"
-        return formatter
-    }
-
-    private var targetFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        return formatter
     }
 }
 
@@ -402,7 +427,7 @@ extension BaseWatchManager: WCSessionDelegate {
             Task {
                 if var preset = tempTargetsStorage.presets().first(where: { $0.id == tempTargetID }) {
                     preset.createdAt = Date()
-                    await tempTargetsStorage.storeTempTargets([preset])
+                    tempTargetsStorage.storeTempTargets([preset])
                     replyHandler(["confirmation": true])
                 } else if tempTargetID == "cancel" {
                     let entry = TempTarget(
@@ -414,7 +439,7 @@ extension BaseWatchManager: WCSessionDelegate {
                         enteredBy: TempTarget.manual,
                         reason: TempTarget.cancel
                     )
-                    await tempTargetsStorage.storeTempTargets([entry])
+                    tempTargetsStorage.storeTempTargets([entry])
                     replyHandler(["confirmation": true])
                 } else {
                     replyHandler(["confirmation": false])
@@ -446,7 +471,6 @@ extension BaseWatchManager: WCSessionDelegate {
 }
 
 extension BaseWatchManager:
-    GlucoseObserver,
     SettingsObserver,
     PumpHistoryObserver,
     PumpSettingsObserver,
@@ -456,12 +480,10 @@ extension BaseWatchManager:
     PumpBatteryObserver,
     PumpReservoirObserver
 {
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-        configureState()
-    }
-
     func settingsDidChange(_: FreeAPSSettings) {
-        configureState()
+        Task {
+            await configureState()
+        }
     }
 
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
@@ -469,7 +491,9 @@ extension BaseWatchManager:
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {
-        configureState()
+        Task {
+            await configureState()
+        }
     }
 
     func basalProfileDidChange(_: [BasalProfileEntry]) {
@@ -477,7 +501,9 @@ extension BaseWatchManager:
     }
 
     func tempTargetsDidUpdate(_: [TempTarget]) {
-        configureState()
+        Task {
+            await configureState()
+        }
     }
 
     func carbsDidUpdate(_: [CarbsEntry]) {

+ 26 - 13
FreeAPS/Sources/Views/TextFieldWithToolBar.swift

@@ -153,14 +153,23 @@ extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
            let currentText = textField.text as NSString?
         {
             // Get the proposed new text
-            let proposedText = currentText.replacingCharacters(in: range, with: string)
+            let proposedTextOriginal = currentText.replacingCharacters(in: range, with: string)
+
+            // Remove thousand separator
+            let proposedText = proposedTextOriginal.replacingOccurrences(of: decimalFormatter.groupingSeparator, with: "")
 
             // Try to convert proposed text to number
             let number = parent.numberFormatter.number(from: proposedText) ?? decimalFormatter.number(from: proposedText)
 
             // Update the binding value if conversion is successful
             if let number = number {
-                parent.text = number.decimalValue
+                let lastCharIndex = proposedText.index(before: proposedText.endIndex)
+                let hasDecimalSeparator = proposedText.contains(decimalFormatter.decimalSeparator)
+                let hasTrailingZeros = (hasDecimalSeparator && proposedText[lastCharIndex] == "0") || isDecimalSeparator
+                if !hasTrailingZeros
+                {
+                    parent.text = number.decimalValue
+                }
             } else {
                 parent.text = 0
             }
@@ -290,20 +299,24 @@ extension TextFieldWithToolBarString.Coordinator: UITextFieldDelegate {
         shouldChangeCharactersIn range: NSRange,
         replacementString string: String
     ) -> Bool {
-        if let maxLength = parent.maxLength {
-            // Get the current text, including the proposed change
-            let currentText = textField.text ?? ""
-            let newLength = currentText.count + string.count - range.length
-            if newLength > maxLength {
-                return false
-            }
+        guard let currentText = textField.text as NSString? else {
+            return false
         }
 
+        // Calculate the new text length
+        let newLength = currentText.length + string.count - range.length
+
+        // If there's a maxLength, ensure the new length is within the limit
+        if let maxLength = parent.maxLength, newLength > maxLength {
+            return false
+        }
+
+        // Attempt to replace characters in range with the replacement string
+        let newText = currentText.replacingCharacters(in: range, with: string)
+
+        // Update the binding text state
         DispatchQueue.main.async {
-            if let textFieldText = textField.text as NSString? {
-                let newText = textFieldText.replacingCharacters(in: range, with: string)
-                self.parent.text = newText
-            }
+            self.parent.text = newText
         }
 
         return true

+ 1 - 1
LoopKit

@@ -1 +1 @@
-Subproject commit 2f535b3ca46825e82e0dd1b5ef9daccd53a3f0ca
+Subproject commit 6d9b19ab7e9f749d573fd42f6bcd2ce4c302cf66

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit 85fc3c6d4805d580acdf6592b220717b6e842558
+Subproject commit 9bb11c714d68f4b915ac484122b4c104fb1c6d3c

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit a80e38b1b7f203014b461f8aff8cead2c067e39d
+Subproject commit f55c08045bf8b3af7b47eaafcc3181bcf5c04681

+ 2 - 2
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "f5c836c216c4ca7d356e3777e58d6d4f9502b03f3974891349eb775f4c4cf750",
+  "originHash" : "59ac7eba66375d6eb406e758cb0b9964f4b3b0ae45c5665596f00384c32262b9",
   "pins" : [
     {
       "identity" : "cryptoswift",
@@ -49,7 +49,7 @@
     {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"