|
@@ -1,3 +1,4 @@
|
|
|
|
|
+import Combine
|
|
|
import CoreData
|
|
import CoreData
|
|
|
import Foundation
|
|
import Foundation
|
|
|
import LoopKit
|
|
import LoopKit
|
|
@@ -25,7 +26,6 @@ extension Bolus {
|
|
|
@Published var insulinRecommended: Decimal = 0
|
|
@Published var insulinRecommended: Decimal = 0
|
|
|
@Published var insulinRequired: Decimal = 0
|
|
@Published var insulinRequired: Decimal = 0
|
|
|
@Published var units: GlucoseUnits = .mgdL
|
|
@Published var units: GlucoseUnits = .mgdL
|
|
|
- @Published var percentage: Decimal = 0
|
|
|
|
|
@Published var threshold: Decimal = 0
|
|
@Published var threshold: Decimal = 0
|
|
|
@Published var maxBolus: Decimal = 0
|
|
@Published var maxBolus: Decimal = 0
|
|
|
var maxExternal: Decimal { maxBolus * 3 }
|
|
var maxExternal: Decimal { maxBolus * 3 }
|
|
@@ -61,7 +61,6 @@ extension Bolus {
|
|
|
@Published var wholeCalc: Decimal = 0
|
|
@Published var wholeCalc: Decimal = 0
|
|
|
@Published var insulinCalculated: Decimal = 0
|
|
@Published var insulinCalculated: Decimal = 0
|
|
|
@Published var fraction: Decimal = 0
|
|
@Published var fraction: Decimal = 0
|
|
|
- @Published var useCalc: Bool = false
|
|
|
|
|
@Published var basal: Decimal = 0
|
|
@Published var basal: Decimal = 0
|
|
|
@Published var fattyMeals: Bool = false
|
|
@Published var fattyMeals: Bool = false
|
|
|
@Published var fattyMealFactor: Decimal = 0
|
|
@Published var fattyMealFactor: Decimal = 0
|
|
@@ -97,7 +96,6 @@ extension Bolus {
|
|
|
|
|
|
|
|
@Published var id_: String = ""
|
|
@Published var id_: String = ""
|
|
|
@Published var summary: String = ""
|
|
@Published var summary: String = ""
|
|
|
- @Published var skipBolus: Bool = false
|
|
|
|
|
|
|
|
|
|
@Published var externalInsulin: Bool = false
|
|
@Published var externalInsulin: Bool = false
|
|
|
@Published var showInfo: Bool = false
|
|
@Published var showInfo: Bool = false
|
|
@@ -111,67 +109,59 @@ extension Bolus {
|
|
|
@Published var minForecast: [Int] = []
|
|
@Published var minForecast: [Int] = []
|
|
|
@Published var maxForecast: [Int] = []
|
|
@Published var maxForecast: [Int] = []
|
|
|
@Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
|
|
@Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
|
|
|
- @Published var displayForecastsAsLines: Bool = false
|
|
|
|
|
- @Published var smooth: Bool = false
|
|
|
|
|
|
|
+ @Published var forecastDisplayType: ForecastDisplayType = .cone
|
|
|
|
|
+ @Published var isSmoothingEnabled: Bool = false
|
|
|
|
|
+ @Published var stops: [Gradient.Stop] = []
|
|
|
|
|
|
|
|
let now = Date.now
|
|
let now = Date.now
|
|
|
|
|
|
|
|
let viewContext = CoreDataStack.shared.persistentContainer.viewContext
|
|
let viewContext = CoreDataStack.shared.persistentContainer.viewContext
|
|
|
- let backgroundContext = CoreDataStack.shared.newTaskContext()
|
|
|
|
|
|
|
+ let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
|
|
|
|
|
+ let determinationFetchContext = CoreDataStack.shared.newTaskContext()
|
|
|
|
|
|
|
|
- private var coreDataObserver: CoreDataObserver?
|
|
|
|
|
|
|
+ private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
|
|
|
|
|
+ private var subscriptions = Set<AnyCancellable>()
|
|
|
|
|
|
|
|
typealias PumpEvent = PumpEventStored.EventType
|
|
typealias PumpEvent = PumpEventStored.EventType
|
|
|
|
|
|
|
|
override func subscribe() {
|
|
override func subscribe() {
|
|
|
- setupGlucoseNotification()
|
|
|
|
|
- coreDataObserver = CoreDataObserver()
|
|
|
|
|
|
|
+ coreDataPublisher =
|
|
|
|
|
+ changedObjectsOnManagedObjectContextDidSavePublisher()
|
|
|
|
|
+ .receive(on: DispatchQueue.global(qos: .background))
|
|
|
|
|
+ .share()
|
|
|
|
|
+ .eraseToAnyPublisher()
|
|
|
registerHandlers()
|
|
registerHandlers()
|
|
|
- setupGlucoseArray()
|
|
|
|
|
|
|
+ registerSubscribers()
|
|
|
|
|
+ setupBolusStateConcurrently()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ private func setupBolusStateConcurrently() {
|
|
|
Task {
|
|
Task {
|
|
|
- async let getAllSettingsDefaults: () = getAllSettingsValues()
|
|
|
|
|
- async let setupDeterminations: () = setupDeterminationsArray()
|
|
|
|
|
-
|
|
|
|
|
- await getAllSettingsDefaults
|
|
|
|
|
- await setupDeterminations
|
|
|
|
|
-
|
|
|
|
|
- // Determination has updated, so we can use this to draw the initial Forecast Chart
|
|
|
|
|
- let forecastData = await mapForecastsForChart()
|
|
|
|
|
- await updateForecasts(with: forecastData)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- broadcaster.register(DeterminationObserver.self, observer: self)
|
|
|
|
|
- broadcaster.register(BolusFailureObserver.self, observer: self)
|
|
|
|
|
- units = settingsManager.settings.units
|
|
|
|
|
- percentage = settingsManager.settings.insulinReqPercentage
|
|
|
|
|
- fraction = settings.settings.overrideFactor
|
|
|
|
|
- useCalc = settings.settings.useCalc
|
|
|
|
|
- fattyMeals = settings.settings.fattyMeals
|
|
|
|
|
- fattyMealFactor = settings.settings.fattyMealFactor
|
|
|
|
|
- sweetMeals = settings.settings.sweetMeals
|
|
|
|
|
- sweetMealFactor = settings.settings.sweetMealFactor
|
|
|
|
|
- displayPresets = settings.settings.displayPresets
|
|
|
|
|
-
|
|
|
|
|
- displayForecastsAsLines = settings.settings.displayForecastsAsLines
|
|
|
|
|
-
|
|
|
|
|
- lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
|
|
|
|
|
- highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
|
|
|
|
|
|
|
+ await withTaskGroup(of: Void.self) { group in
|
|
|
|
|
+ group.addTask {
|
|
|
|
|
+ self.setupGlucoseArray()
|
|
|
|
|
+ }
|
|
|
|
|
+ group.addTask {
|
|
|
|
|
+ self.setupDeterminationsAndForecasts()
|
|
|
|
|
+ }
|
|
|
|
|
+ group.addTask {
|
|
|
|
|
+ await self.setupSettings()
|
|
|
|
|
+ }
|
|
|
|
|
+ group.addTask {
|
|
|
|
|
+ self.registerObservers()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- maxCarbs = settings.settings.maxCarbs
|
|
|
|
|
- maxFat = settings.settings.maxFat
|
|
|
|
|
- maxProtein = settings.settings.maxProtein
|
|
|
|
|
- skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
|
|
|
|
|
- useFPUconversion = settingsManager.settings.useFPUconversion
|
|
|
|
|
- smooth = settingsManager.settings.smoothGlucose
|
|
|
|
|
-
|
|
|
|
|
- if waitForSuggestionInitial {
|
|
|
|
|
- Task {
|
|
|
|
|
- let ok = await apsManager.determineBasal()
|
|
|
|
|
- if !ok {
|
|
|
|
|
- self.waitForSuggestion = false
|
|
|
|
|
- self.insulinRequired = 0
|
|
|
|
|
- self.insulinRecommended = 0
|
|
|
|
|
|
|
+ if self.waitForSuggestionInitial {
|
|
|
|
|
+ group.addTask {
|
|
|
|
|
+ let isDetermineBasalSuccessful = await self.apsManager.determineBasal()
|
|
|
|
|
+ if !isDetermineBasalSuccessful {
|
|
|
|
|
+ await MainActor.run {
|
|
|
|
|
+ self.waitForSuggestion = false
|
|
|
|
|
+ self.insulinRequired = 0
|
|
|
|
|
+ self.insulinRecommended = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -209,6 +199,43 @@ extension Bolus {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func setupDeterminationsAndForecasts() {
|
|
|
|
|
+ Task {
|
|
|
|
|
+ async let getAllSettingsDefaults: () = getAllSettingsValues()
|
|
|
|
|
+ async let setupDeterminations: () = setupDeterminationsArray()
|
|
|
|
|
+
|
|
|
|
|
+ await getAllSettingsDefaults
|
|
|
|
|
+ await setupDeterminations
|
|
|
|
|
+
|
|
|
|
|
+ // Determination has updated, so we can use this to draw the initial Forecast Chart
|
|
|
|
|
+ let forecastData = await mapForecastsForChart()
|
|
|
|
|
+ await updateForecasts(with: forecastData)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func registerObservers() {
|
|
|
|
|
+ broadcaster.register(DeterminationObserver.self, observer: self)
|
|
|
|
|
+ broadcaster.register(BolusFailureObserver.self, observer: self)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @MainActor private func setupSettings() async {
|
|
|
|
|
+ units = settingsManager.settings.units
|
|
|
|
|
+ fraction = settings.settings.overrideFactor
|
|
|
|
|
+ fattyMeals = settings.settings.fattyMeals
|
|
|
|
|
+ fattyMealFactor = settings.settings.fattyMealFactor
|
|
|
|
|
+ sweetMeals = settings.settings.sweetMeals
|
|
|
|
|
+ sweetMealFactor = settings.settings.sweetMealFactor
|
|
|
|
|
+ displayPresets = settings.settings.displayPresets
|
|
|
|
|
+ forecastDisplayType = settings.settings.forecastDisplayType
|
|
|
|
|
+ lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
|
|
|
|
|
+ highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
|
|
|
|
|
+ maxCarbs = settings.settings.maxCarbs
|
|
|
|
|
+ maxFat = settings.settings.maxFat
|
|
|
|
|
+ maxProtein = settings.settings.maxProtein
|
|
|
|
|
+ useFPUconversion = settingsManager.settings.useFPUconversion
|
|
|
|
|
+ isSmoothingEnabled = settingsManager.settings.smoothGlucose
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func getCurrentSettingValue(for type: SettingType) async {
|
|
private func getCurrentSettingValue(for type: SettingType) async {
|
|
|
let now = Date()
|
|
let now = Date()
|
|
|
let calendar = Calendar.current
|
|
let calendar = Calendar.current
|
|
@@ -284,7 +311,7 @@ extension Bolus {
|
|
|
|
|
|
|
|
/// Calculate insulin recommendation
|
|
/// Calculate insulin recommendation
|
|
|
func calculateInsulin() -> Decimal {
|
|
func calculateInsulin() -> Decimal {
|
|
|
- let isfForCalculation = units == .mmolL ? isf.asMgdL : isf
|
|
|
|
|
|
|
+ let isfForCalculation = isf
|
|
|
|
|
|
|
|
// insulin needed for the current blood glucose
|
|
// insulin needed for the current blood glucose
|
|
|
targetDifference = currentBG - target
|
|
targetDifference = currentBG - target
|
|
@@ -363,9 +390,16 @@ extension Bolus {
|
|
|
|
|
|
|
|
await saveMeal()
|
|
await saveMeal()
|
|
|
|
|
|
|
|
- // if glucose data is stale end the custom loading animation by hiding the modal
|
|
|
|
|
- guard glucoseStorage.isGlucoseDataFresh(glucoseFromPersistence.first?.date) else {
|
|
|
|
|
- waitForSuggestion = false
|
|
|
|
|
|
|
+ // If glucose data is stale end the custom loading animation by hiding the modal
|
|
|
|
|
+ // Get date on Main thread
|
|
|
|
|
+ let date = await MainActor.run {
|
|
|
|
|
+ glucoseFromPersistence.first?.date
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ guard glucoseStorage.isGlucoseDataFresh(date) else {
|
|
|
|
|
+ await MainActor.run {
|
|
|
|
|
+ waitForSuggestion = false
|
|
|
|
|
+ }
|
|
|
return hideModal()
|
|
return hideModal()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -467,7 +501,7 @@ extension Bolus {
|
|
|
enteredBy: CarbsEntry.manual,
|
|
enteredBy: CarbsEntry.manual,
|
|
|
isFPU: false, fpuID: UUID().uuidString
|
|
isFPU: false, fpuID: UUID().uuidString
|
|
|
)]
|
|
)]
|
|
|
- await carbsStorage.storeCarbs(carbsToStore)
|
|
|
|
|
|
|
+ await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
|
|
|
|
|
|
|
|
if carbs > 0 || fat > 0 || protein > 0 {
|
|
if carbs > 0 || fat > 0 || protein > 0 {
|
|
|
// only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
|
|
// only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
|
|
@@ -542,33 +576,29 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
|
|
|
|
|
|
|
|
extension Bolus.StateModel {
|
|
extension Bolus.StateModel {
|
|
|
private func registerHandlers() {
|
|
private func registerHandlers() {
|
|
|
- coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
|
|
|
|
|
|
|
+ coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
|
|
|
guard let self = self else { return }
|
|
guard let self = self else { return }
|
|
|
Task {
|
|
Task {
|
|
|
await self.setupDeterminationsArray()
|
|
await self.setupDeterminationsArray()
|
|
|
await self.updateForecasts()
|
|
await self.updateForecasts()
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+ }.store(in: &subscriptions)
|
|
|
|
|
|
|
|
// Due to the Batch insert this only is used for observing Deletion of Glucose entries
|
|
// Due to the Batch insert this only is used for observing Deletion of Glucose entries
|
|
|
- coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
|
|
|
|
|
|
|
+ coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
|
|
|
guard let self = self else { return }
|
|
guard let self = self else { return }
|
|
|
self.setupGlucoseArray()
|
|
self.setupGlucoseArray()
|
|
|
- }
|
|
|
|
|
|
|
+ }.store(in: &subscriptions)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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() {
|
|
|
|
|
- setupGlucoseArray()
|
|
|
|
|
|
|
+ private func registerSubscribers() {
|
|
|
|
|
+ glucoseStorage.updatePublisher
|
|
|
|
|
+ .receive(on: DispatchQueue.global(qos: .background))
|
|
|
|
|
+ .sink { [weak self] _ in
|
|
|
|
|
+ guard let self = self else { return }
|
|
|
|
|
+ self.setupGlucoseArray()
|
|
|
|
|
+ }
|
|
|
|
|
+ .store(in: &subscriptions)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -587,16 +617,16 @@ extension Bolus.StateModel {
|
|
|
private func fetchGlucose() async -> [NSManagedObjectID] {
|
|
private func fetchGlucose() async -> [NSManagedObjectID] {
|
|
|
let results = await CoreDataStack.shared.fetchEntitiesAsync(
|
|
let results = await CoreDataStack.shared.fetchEntitiesAsync(
|
|
|
ofType: GlucoseStored.self,
|
|
ofType: GlucoseStored.self,
|
|
|
- onContext: backgroundContext,
|
|
|
|
|
|
|
+ onContext: glucoseFetchContext,
|
|
|
predicate: NSPredicate.glucose,
|
|
predicate: NSPredicate.glucose,
|
|
|
key: "date",
|
|
key: "date",
|
|
|
ascending: false,
|
|
ascending: false,
|
|
|
fetchLimit: 288
|
|
fetchLimit: 288
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- guard let fetchedResults = results as? [GlucoseStored] else { return [] }
|
|
|
|
|
|
|
+ return await glucoseFetchContext.perform {
|
|
|
|
|
+ guard let fetchedResults = results as? [GlucoseStored] else { return [] }
|
|
|
|
|
|
|
|
- return await backgroundContext.perform {
|
|
|
|
|
return fetchedResults.map(\.objectID)
|
|
return fetchedResults.map(\.objectID)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -632,16 +662,16 @@ extension Bolus.StateModel {
|
|
|
|
|
|
|
|
private func mapForecastsForChart() async -> Determination? {
|
|
private func mapForecastsForChart() async -> Determination? {
|
|
|
let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
|
|
let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
|
|
|
- .getNSManagedObject(with: determinationObjectIDs, context: backgroundContext)
|
|
|
|
|
|
|
+ .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
|
|
|
|
|
|
|
|
- return await backgroundContext.perform {
|
|
|
|
|
|
|
+ return await determinationFetchContext.perform {
|
|
|
guard let determinationObject = determinationObjects.first else {
|
|
guard let determinationObject = determinationObjects.first else {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let eventualBG = determinationObject.eventualBG?.intValue
|
|
let eventualBG = determinationObject.eventualBG?.intValue
|
|
|
|
|
|
|
|
- let forecastsSet = determinationObject.forecasts as? Set<Forecast> ?? []
|
|
|
|
|
|
|
+ let forecastsSet = determinationObject.forecasts ?? []
|
|
|
let predictions = Predictions(
|
|
let predictions = Predictions(
|
|
|
iob: forecastsSet.extractValues(for: "iob"),
|
|
iob: forecastsSet.extractValues(for: "iob"),
|
|
|
zt: forecastsSet.extractValues(for: "zt"),
|
|
zt: forecastsSet.extractValues(for: "zt"),
|
|
@@ -730,20 +760,20 @@ extension Bolus.StateModel {
|
|
|
minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
|
|
minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
|
|
|
guard minCount > 0 else { return }
|
|
guard minCount > 0 else { return }
|
|
|
|
|
|
|
|
- let (minResult, maxResult) = await Task.detached {
|
|
|
|
|
- let minForecast = (0 ..< self.minCount).map { index in
|
|
|
|
|
|
|
+ async let minForecastResult = Task.detached {
|
|
|
|
|
+ (0 ..< self.minCount).map { index in
|
|
|
nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
|
|
nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
|
|
|
}
|
|
}
|
|
|
|
|
+ }.value
|
|
|
|
|
|
|
|
- let maxForecast = (0 ..< self.minCount).map { index in
|
|
|
|
|
|
|
+ async let maxForecastResult = Task.detached {
|
|
|
|
|
+ (0 ..< self.minCount).map { index in
|
|
|
nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
|
|
nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return (minForecast, maxForecast)
|
|
|
|
|
}.value
|
|
}.value
|
|
|
|
|
|
|
|
- minForecast = minResult
|
|
|
|
|
- maxForecast = maxResult
|
|
|
|
|
|
|
+ minForecast = await minForecastResult
|
|
|
|
|
+ maxForecast = await maxForecastResult
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|