|
|
@@ -17,6 +17,9 @@ extension Bolus {
|
|
|
@Injected() var glucoseStorage: GlucoseStorage!
|
|
|
@Injected() var determinationStorage: DeterminationStorage!
|
|
|
|
|
|
+ @Published var lowGlucose: Decimal = 70
|
|
|
+ @Published var highGlucose: Decimal = 180
|
|
|
+
|
|
|
@Published var predictions: Predictions?
|
|
|
@Published var amount: Decimal = 0
|
|
|
@Published var insulinRecommended: Decimal = 0
|
|
|
@@ -25,6 +28,7 @@ extension Bolus {
|
|
|
@Published var percentage: Decimal = 0
|
|
|
@Published var threshold: Decimal = 0
|
|
|
@Published var maxBolus: Decimal = 0
|
|
|
+ var maxExternal: Decimal { maxBolus * 3 }
|
|
|
@Published var errorString: Decimal = 0
|
|
|
@Published var evBG: Decimal = 0
|
|
|
@Published var insulin: Decimal = 0
|
|
|
@@ -65,6 +69,10 @@ extension Bolus {
|
|
|
@Published var displayPresets: Bool = true
|
|
|
|
|
|
@Published var currentBasal: Decimal = 0
|
|
|
+ @Published var currentCarbRatio: Decimal = 0
|
|
|
+ @Published var currentBGTarget: Decimal = 0
|
|
|
+ @Published var currentISF: Decimal = 0
|
|
|
+
|
|
|
@Published var sweetMeals: Bool = false
|
|
|
@Published var sweetMealFactor: Decimal = 0
|
|
|
@Published var useSuperBolus: Bool = false
|
|
|
@@ -84,6 +92,8 @@ extension Bolus {
|
|
|
@Published var selection: MealPresetStored?
|
|
|
@Published var summation: [String] = []
|
|
|
@Published var maxCarbs: Decimal = 0
|
|
|
+ @Published var maxFat: Decimal = 0
|
|
|
+ @Published var maxProtein: Decimal = 0
|
|
|
|
|
|
@Published var id_: String = ""
|
|
|
@Published var summary: String = ""
|
|
|
@@ -93,10 +103,20 @@ extension Bolus {
|
|
|
@Published var showInfo: Bool = false
|
|
|
@Published var glucoseFromPersistence: [GlucoseStored] = []
|
|
|
@Published var determination: [OrefDetermination] = []
|
|
|
+ @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
|
|
|
+ @Published var predictionsForChart: Predictions?
|
|
|
+ @Published var simulatedDetermination: Determination?
|
|
|
+ @Published var determinationObjectIDs: [NSManagedObjectID] = []
|
|
|
+
|
|
|
+ @Published var minForecast: [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 displayForecastsAsLines: Bool = false
|
|
|
+ @Published var smooth: Bool = false
|
|
|
|
|
|
let now = Date.now
|
|
|
|
|
|
- let context = CoreDataStack.shared.persistentContainer.viewContext
|
|
|
+ let viewContext = CoreDataStack.shared.persistentContainer.viewContext
|
|
|
let backgroundContext = CoreDataStack.shared.newTaskContext()
|
|
|
|
|
|
private var coreDataObserver: CoreDataObserver?
|
|
|
@@ -107,16 +127,24 @@ extension Bolus {
|
|
|
setupGlucoseNotification()
|
|
|
coreDataObserver = CoreDataObserver()
|
|
|
registerHandlers()
|
|
|
-
|
|
|
setupGlucoseArray()
|
|
|
- setupDeterminationsArray()
|
|
|
+
|
|
|
+ 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
|
|
|
- maxBolus = provider.pumpSettings().maxBolus
|
|
|
- // added
|
|
|
fraction = settings.settings.overrideFactor
|
|
|
useCalc = settings.settings.useCalc
|
|
|
fattyMeals = settings.settings.fattyMeals
|
|
|
@@ -125,9 +153,17 @@ extension Bolus {
|
|
|
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
|
|
|
+
|
|
|
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 {
|
|
|
@@ -143,47 +179,103 @@ extension Bolus {
|
|
|
|
|
|
// MARK: - Basal
|
|
|
|
|
|
- func getCurrentBasal() {
|
|
|
- let basalEntries = provider.getProfile()
|
|
|
+ private enum SettingType {
|
|
|
+ case basal
|
|
|
+ case carbRatio
|
|
|
+ case bgTarget
|
|
|
+ case isf
|
|
|
+ }
|
|
|
+
|
|
|
+ func getAllSettingsValues() async {
|
|
|
+ await withTaskGroup(of: Void.self) { group in
|
|
|
+ group.addTask {
|
|
|
+ await self.getCurrentSettingValue(for: .basal)
|
|
|
+ }
|
|
|
+ group.addTask {
|
|
|
+ await self.getCurrentSettingValue(for: .carbRatio)
|
|
|
+ }
|
|
|
+ group.addTask {
|
|
|
+ await self.getCurrentSettingValue(for: .bgTarget)
|
|
|
+ }
|
|
|
+ group.addTask {
|
|
|
+ await self.getCurrentSettingValue(for: .isf)
|
|
|
+ }
|
|
|
+ group.addTask {
|
|
|
+ let getMaxBolus = await self.provider.getPumpSettings().maxBolus
|
|
|
+ await MainActor.run {
|
|
|
+ self.maxBolus = getMaxBolus
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func getCurrentSettingValue(for type: SettingType) async {
|
|
|
let now = Date()
|
|
|
let calendar = Calendar.current
|
|
|
let dateFormatter = DateFormatter()
|
|
|
dateFormatter.dateFormat = "HH:mm:ss"
|
|
|
dateFormatter.timeZone = TimeZone.current
|
|
|
|
|
|
- for (index, entry) in basalEntries.enumerated() {
|
|
|
+ let entries: [(start: String, value: Decimal)]
|
|
|
+
|
|
|
+ switch type {
|
|
|
+ case .basal:
|
|
|
+ let basalEntries = await provider.getBasalProfile()
|
|
|
+ entries = basalEntries.map { ($0.start, $0.rate) }
|
|
|
+ case .carbRatio:
|
|
|
+ let carbRatios = await provider.getCarbRatios()
|
|
|
+ entries = carbRatios.schedule.map { ($0.start, $0.ratio) }
|
|
|
+ case .bgTarget:
|
|
|
+ let bgTargets = await provider.getBGTarget()
|
|
|
+ entries = bgTargets.targets.map { ($0.start, $0.low) }
|
|
|
+ case .isf:
|
|
|
+ let isfValues = await provider.getISFValues()
|
|
|
+ entries = isfValues.sensitivities.map { ($0.start, $0.sensitivity) }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (index, entry) in entries.enumerated() {
|
|
|
guard let entryTime = dateFormatter.date(from: entry.start) else {
|
|
|
print("Invalid entry start time: \(entry.start)")
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- // Combine the current date with the time from entry.start
|
|
|
+ let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
|
|
|
let entryStartTime = calendar.date(
|
|
|
- bySettingHour: calendar.component(.hour, from: entryTime),
|
|
|
- minute: calendar.component(.minute, from: entryTime),
|
|
|
- second: calendar.component(.second, from: entryTime),
|
|
|
+ bySettingHour: entryComponents.hour!,
|
|
|
+ minute: entryComponents.minute!,
|
|
|
+ second: entryComponents.second!,
|
|
|
of: now
|
|
|
)!
|
|
|
|
|
|
let entryEndTime: Date
|
|
|
- if index < basalEntries.count - 1,
|
|
|
- let nextEntryTime = dateFormatter.date(from: basalEntries[index + 1].start)
|
|
|
+ if index < entries.count - 1,
|
|
|
+ let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
|
|
|
{
|
|
|
- let nextEntryStartTime = calendar.date(
|
|
|
- bySettingHour: calendar.component(.hour, from: nextEntryTime),
|
|
|
- minute: calendar.component(.minute, from: nextEntryTime),
|
|
|
- second: calendar.component(.second, from: nextEntryTime),
|
|
|
+ let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
|
|
|
+ entryEndTime = calendar.date(
|
|
|
+ bySettingHour: nextEntryComponents.hour!,
|
|
|
+ minute: nextEntryComponents.minute!,
|
|
|
+ second: nextEntryComponents.second!,
|
|
|
of: now
|
|
|
)!
|
|
|
- entryEndTime = nextEntryStartTime
|
|
|
} else {
|
|
|
- // If it's the last entry, use the same start time plus one day as the end time
|
|
|
entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
|
|
|
}
|
|
|
|
|
|
if now >= entryStartTime, now < entryEndTime {
|
|
|
- currentBasal = entry.rate
|
|
|
- break
|
|
|
+ await MainActor.run {
|
|
|
+ switch type {
|
|
|
+ case .basal:
|
|
|
+ currentBasal = entry.value
|
|
|
+ case .carbRatio:
|
|
|
+ currentCarbRatio = entry.value
|
|
|
+ case .bgTarget:
|
|
|
+ currentBGTarget = entry.value
|
|
|
+ case .isf:
|
|
|
+ currentISF = entry.value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -192,11 +284,7 @@ extension Bolus {
|
|
|
|
|
|
/// Calculate insulin recommendation
|
|
|
func calculateInsulin() -> Decimal {
|
|
|
- // ensure that isf is in mg/dL
|
|
|
- var conversion: Decimal {
|
|
|
- units == .mmolL ? 0.0555 : 1
|
|
|
- }
|
|
|
- let isfForCalculation = isf / conversion
|
|
|
+ let isfForCalculation = units == .mmolL ? isf.asMgdL : isf
|
|
|
|
|
|
// insulin needed for the current blood glucose
|
|
|
targetDifference = currentBG - target
|
|
|
@@ -252,9 +340,11 @@ extension Bolus {
|
|
|
|
|
|
// MARK: - Button tasks
|
|
|
|
|
|
- @MainActor func invokeTreatmentsTask() {
|
|
|
+ func invokeTreatmentsTask() {
|
|
|
Task {
|
|
|
- addButtonPressed = true
|
|
|
+ await MainActor.run {
|
|
|
+ self.addButtonPressed = true
|
|
|
+ }
|
|
|
let isInsulinGiven = amount > 0
|
|
|
let isCarbsPresent = carbs > 0
|
|
|
let isFatPresent = fat > 0
|
|
|
@@ -263,7 +353,9 @@ extension Bolus {
|
|
|
if isInsulinGiven {
|
|
|
try await handleInsulin(isExternal: externalInsulin)
|
|
|
} else if isCarbsPresent || isFatPresent || isProteinPresent {
|
|
|
- waitForSuggestion = true
|
|
|
+ await MainActor.run {
|
|
|
+ self.waitForSuggestion = true
|
|
|
+ }
|
|
|
} else {
|
|
|
hideModal()
|
|
|
return
|
|
|
@@ -272,24 +364,28 @@ extension Bolus {
|
|
|
await saveMeal()
|
|
|
|
|
|
// if glucose data is stale end the custom loading animation by hiding the modal
|
|
|
-// guard glucoseOfLast20Min.first?.date ?? now >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
|
|
|
-// return hideModal()
|
|
|
-// }
|
|
|
+ guard glucoseStorage.isGlucoseDataFresh(glucoseFromPersistence.first?.date) else {
|
|
|
+ waitForSuggestion = false
|
|
|
+ return hideModal()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - Insulin
|
|
|
|
|
|
- @MainActor private func handleInsulin(isExternal: Bool) async throws {
|
|
|
+ private func handleInsulin(isExternal: Bool) async throws {
|
|
|
if !isExternal {
|
|
|
await addPumpInsulin()
|
|
|
} else {
|
|
|
await addExternalInsulin()
|
|
|
}
|
|
|
- waitForSuggestion = true
|
|
|
+
|
|
|
+ await MainActor.run {
|
|
|
+ self.waitForSuggestion = true
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- @MainActor func addPumpInsulin() async {
|
|
|
+ func addPumpInsulin() async {
|
|
|
guard amount > 0 else {
|
|
|
showModal(for: nil)
|
|
|
return
|
|
|
@@ -306,7 +402,7 @@ extension Bolus {
|
|
|
}
|
|
|
} catch {
|
|
|
print("authentication error for pump bolus: \(error.localizedDescription)")
|
|
|
- DispatchQueue.main.async {
|
|
|
+ await MainActor.run {
|
|
|
self.waitForSuggestion = false
|
|
|
if self.addButtonPressed {
|
|
|
self.hideModal()
|
|
|
@@ -315,38 +411,17 @@ extension Bolus {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func savePumpInsulin(amount _: Decimal) {
|
|
|
- context.perform {
|
|
|
- // create pump event
|
|
|
- let newPumpEvent = PumpEventStored(context: self.context)
|
|
|
- newPumpEvent.timestamp = Date()
|
|
|
- newPumpEvent.type = PumpEvent.bolus.rawValue
|
|
|
-
|
|
|
- // create bolus entry and specify relationship to pump event
|
|
|
- let newBolusEntry = BolusStored(context: self.context)
|
|
|
- newBolusEntry.pumpEvent = newPumpEvent
|
|
|
- newBolusEntry.amount = self.amount as NSDecimalNumber
|
|
|
- newBolusEntry.isExternal = false
|
|
|
- newBolusEntry.isSMB = false
|
|
|
-
|
|
|
- do {
|
|
|
- guard self.context.hasChanges else { return }
|
|
|
- try self.context.save()
|
|
|
- } catch {
|
|
|
- print(error.localizedDescription)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// MARK: - EXTERNAL INSULIN
|
|
|
|
|
|
- @MainActor func addExternalInsulin() async {
|
|
|
+ func addExternalInsulin() async {
|
|
|
guard amount > 0 else {
|
|
|
showModal(for: nil)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- amount = min(amount, maxBolus * 3)
|
|
|
+ await MainActor.run {
|
|
|
+ self.amount = min(self.amount, self.maxBolus * 3)
|
|
|
+ }
|
|
|
|
|
|
do {
|
|
|
let authenticated = try await unlockmanager.unlock()
|
|
|
@@ -360,7 +435,7 @@ extension Bolus {
|
|
|
}
|
|
|
} catch {
|
|
|
print("authentication error for external insulin: \(error.localizedDescription)")
|
|
|
- DispatchQueue.main.async {
|
|
|
+ await MainActor.run {
|
|
|
self.waitForSuggestion = false
|
|
|
if self.addButtonPressed {
|
|
|
self.hideModal()
|
|
|
@@ -371,10 +446,15 @@ extension Bolus {
|
|
|
|
|
|
// MARK: - Carbs
|
|
|
|
|
|
- @MainActor func saveMeal() async {
|
|
|
+ func saveMeal() async {
|
|
|
guard carbs > 0 || fat > 0 || protein > 0 else { return }
|
|
|
- carbs = min(carbs, maxCarbs)
|
|
|
- id_ = UUID().uuidString
|
|
|
+
|
|
|
+ await MainActor.run {
|
|
|
+ self.carbs = min(self.carbs, self.maxCarbs)
|
|
|
+ self.fat = min(self.fat, self.maxFat)
|
|
|
+ self.protein = min(self.protein, self.maxProtein)
|
|
|
+ self.id_ = UUID().uuidString
|
|
|
+ }
|
|
|
|
|
|
let carbsToStore = [CarbsEntry(
|
|
|
id: id_,
|
|
|
@@ -401,11 +481,11 @@ extension Bolus {
|
|
|
|
|
|
func deletePreset() {
|
|
|
if selection != nil {
|
|
|
- context.delete(selection!)
|
|
|
+ viewContext.delete(selection!)
|
|
|
|
|
|
do {
|
|
|
- guard context.hasChanges else { return }
|
|
|
- try context.save()
|
|
|
+ guard viewContext.hasChanges else { return }
|
|
|
+ try viewContext.save()
|
|
|
} catch {
|
|
|
print(error.localizedDescription)
|
|
|
}
|
|
|
@@ -437,79 +517,6 @@ extension Bolus {
|
|
|
func addToSummation() {
|
|
|
summation.append(selection?.dish ?? "")
|
|
|
}
|
|
|
-
|
|
|
- func waitersNotepad() -> String {
|
|
|
- var filteredArray = summation.filter { !$0.isEmpty }
|
|
|
-
|
|
|
- if carbs == 0, protein == 0, fat == 0 {
|
|
|
- filteredArray = []
|
|
|
- }
|
|
|
-
|
|
|
- guard filteredArray != [] else {
|
|
|
- return ""
|
|
|
- }
|
|
|
- var carbs_: Decimal = 0.0
|
|
|
- var fat_: Decimal = 0.0
|
|
|
- var protein_: Decimal = 0.0
|
|
|
- var presetArray = [MealPresetStored]()
|
|
|
-
|
|
|
- context.performAndWait {
|
|
|
- let requestPresets = MealPresetStored.fetchRequest() as NSFetchRequest<MealPresetStored>
|
|
|
- try? presetArray = context.fetch(requestPresets)
|
|
|
- }
|
|
|
- var waitersNotepad = [String]()
|
|
|
- var stringValue = ""
|
|
|
-
|
|
|
- for each in filteredArray {
|
|
|
- let countedSet = NSCountedSet(array: filteredArray)
|
|
|
- let count = countedSet.count(for: each)
|
|
|
- if each != stringValue {
|
|
|
- waitersNotepad.append("\(count) \(each)")
|
|
|
- }
|
|
|
- stringValue = each
|
|
|
-
|
|
|
- for sel in presetArray {
|
|
|
- if sel.dish == each {
|
|
|
- carbs_ += (sel.carbs)! as Decimal
|
|
|
- fat_ += (sel.fat)! as Decimal
|
|
|
- protein_ += (sel.protein)! as Decimal
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- let extracarbs = carbs - carbs_
|
|
|
- let extraFat = fat - fat_
|
|
|
- let extraProtein = protein - protein_
|
|
|
- var addedString = ""
|
|
|
-
|
|
|
- if extracarbs > 0, filteredArray.isNotEmpty {
|
|
|
- addedString += "Additional carbs: \(extracarbs) ,"
|
|
|
- } else if extracarbs < 0 { addedString += "Removed carbs: \(extracarbs) " }
|
|
|
-
|
|
|
- if extraFat > 0, filteredArray.isNotEmpty {
|
|
|
- addedString += "Additional fat: \(extraFat) ,"
|
|
|
- } else if extraFat < 0 { addedString += "Removed fat: \(extraFat) ," }
|
|
|
-
|
|
|
- if extraProtein > 0, filteredArray.isNotEmpty {
|
|
|
- addedString += "Additional protein: \(extraProtein) ,"
|
|
|
- } else if extraProtein < 0 { addedString += "Removed protein: \(extraProtein) ," }
|
|
|
-
|
|
|
- if addedString != "" {
|
|
|
- waitersNotepad.append(addedString)
|
|
|
- }
|
|
|
- var waitersNotepadString = ""
|
|
|
-
|
|
|
- if waitersNotepad.count == 1 {
|
|
|
- waitersNotepadString = waitersNotepad[0]
|
|
|
- } else if waitersNotepad.count > 1 {
|
|
|
- for each in waitersNotepad {
|
|
|
- if each != waitersNotepad.last {
|
|
|
- waitersNotepadString += " " + each + ","
|
|
|
- } else { waitersNotepadString += " " + each }
|
|
|
- }
|
|
|
- }
|
|
|
- return waitersNotepadString
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -537,7 +544,10 @@ extension Bolus.StateModel {
|
|
|
private func registerHandlers() {
|
|
|
coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
|
|
|
guard let self = self else { return }
|
|
|
- self.setupDeterminationsArray()
|
|
|
+ Task {
|
|
|
+ await self.setupDeterminationsArray()
|
|
|
+ await self.updateForecasts()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Due to the Batch insert this only is used for observing Deletion of Glucose entries
|
|
|
@@ -569,7 +579,8 @@ extension Bolus.StateModel {
|
|
|
private func setupGlucoseArray() {
|
|
|
Task {
|
|
|
let ids = await self.fetchGlucose()
|
|
|
- await updateGlucoseArray(with: ids)
|
|
|
+ let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
|
|
|
+ await updateGlucoseArray(with: glucoseObjects)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -577,72 +588,177 @@ extension Bolus.StateModel {
|
|
|
let results = await CoreDataStack.shared.fetchEntitiesAsync(
|
|
|
ofType: GlucoseStored.self,
|
|
|
onContext: backgroundContext,
|
|
|
- predicate: NSPredicate.predicateFor30MinAgo,
|
|
|
+ predicate: NSPredicate.glucose,
|
|
|
key: "date",
|
|
|
ascending: false,
|
|
|
- fetchLimit: 3
|
|
|
+ fetchLimit: 288
|
|
|
)
|
|
|
|
|
|
+ guard let fetchedResults = results as? [GlucoseStored] else { return [] }
|
|
|
+
|
|
|
return await backgroundContext.perform {
|
|
|
- return results.map(\.objectID)
|
|
|
+ return fetchedResults.map(\.objectID)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
|
|
|
- do {
|
|
|
- let glucoseObjects = try IDs.compactMap { id in
|
|
|
- try context.existingObject(with: id) as? GlucoseStored
|
|
|
- }
|
|
|
- glucoseFromPersistence = glucoseObjects
|
|
|
+ @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
|
|
|
+ glucoseFromPersistence = objects
|
|
|
|
|
|
- let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
|
|
|
- let thirdLastGlucose = glucoseFromPersistence.last?.glucose ?? 0
|
|
|
- let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
|
|
|
+ let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
|
|
|
+ let thirdLastGlucose = glucoseFromPersistence.dropFirst(2).first?.glucose ?? 0
|
|
|
+ let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
|
|
|
|
|
|
- currentBG = Decimal(lastGlucose)
|
|
|
- deltaBG = delta
|
|
|
- } catch {
|
|
|
- debugPrint(
|
|
|
- "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
|
|
|
- )
|
|
|
- }
|
|
|
+ currentBG = Decimal(lastGlucose)
|
|
|
+ deltaBG = delta
|
|
|
}
|
|
|
|
|
|
// Determinations
|
|
|
- private func setupDeterminationsArray() {
|
|
|
- Task {
|
|
|
- let ids = await determinationStorage.fetchLastDeterminationObjectID(
|
|
|
- predicate: NSPredicate.predicateFor30MinAgoForDetermination
|
|
|
- )
|
|
|
- await updateDeterminationsArray(with: ids)
|
|
|
+ private func setupDeterminationsArray() async {
|
|
|
+ // Fetch object IDs on a background thread
|
|
|
+ let fetchedObjectIDs = await determinationStorage.fetchLastDeterminationObjectID(
|
|
|
+ predicate: NSPredicate.predicateFor30MinAgoForDetermination
|
|
|
+ )
|
|
|
+
|
|
|
+ // Update determinationObjectIDs on the main thread
|
|
|
+ await MainActor.run {
|
|
|
+ determinationObjectIDs = fetchedObjectIDs
|
|
|
}
|
|
|
+
|
|
|
+ let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
|
|
|
+ .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
|
|
|
+
|
|
|
+ await updateDeterminationsArray(with: determinationObjects)
|
|
|
}
|
|
|
|
|
|
- @MainActor private func updateDeterminationsArray(with IDs: [NSManagedObjectID]) {
|
|
|
- do {
|
|
|
- let determinationObjects = try IDs.compactMap { id in
|
|
|
- try context.existingObject(with: id) as? OrefDetermination
|
|
|
+ private func mapForecastsForChart() async -> Determination? {
|
|
|
+ let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
|
|
|
+ .getNSManagedObject(with: determinationObjectIDs, context: backgroundContext)
|
|
|
+
|
|
|
+ return await backgroundContext.perform {
|
|
|
+ guard let determinationObject = determinationObjects.first else {
|
|
|
+ return nil
|
|
|
}
|
|
|
- guard let mostRecentDetermination = determinationObjects.first else { return }
|
|
|
- determination = determinationObjects
|
|
|
-
|
|
|
- // setup vars for bolus calculation
|
|
|
- insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
|
|
|
- evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
|
|
|
- insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
|
|
|
- target = (mostRecentDetermination.currentTarget ?? 100) as Decimal
|
|
|
- isf = (mostRecentDetermination.insulinSensitivity ?? 0) as Decimal
|
|
|
- cob = mostRecentDetermination.cob as Int16
|
|
|
- iob = (mostRecentDetermination.iob ?? 0) as Decimal
|
|
|
- basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
|
|
|
- carbRatio = (mostRecentDetermination.carbRatio ?? 0) as Decimal
|
|
|
-
|
|
|
- getCurrentBasal()
|
|
|
- insulinCalculated = calculateInsulin()
|
|
|
- } catch {
|
|
|
- debugPrint(
|
|
|
- "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the determinations array: \(error.localizedDescription)"
|
|
|
+
|
|
|
+ let eventualBG = determinationObject.eventualBG?.intValue
|
|
|
+
|
|
|
+ let forecastsSet = determinationObject.forecasts as? Set<Forecast> ?? []
|
|
|
+ let predictions = Predictions(
|
|
|
+ iob: forecastsSet.extractValues(for: "iob"),
|
|
|
+ zt: forecastsSet.extractValues(for: "zt"),
|
|
|
+ cob: forecastsSet.extractValues(for: "cob"),
|
|
|
+ uam: forecastsSet.extractValues(for: "uam")
|
|
|
+ )
|
|
|
+
|
|
|
+ return Determination(
|
|
|
+ id: UUID(),
|
|
|
+ reason: "",
|
|
|
+ units: 0,
|
|
|
+ insulinReq: 0,
|
|
|
+ eventualBG: eventualBG,
|
|
|
+ sensitivityRatio: 0,
|
|
|
+ rate: 0,
|
|
|
+ duration: 0,
|
|
|
+ iob: 0,
|
|
|
+ cob: 0,
|
|
|
+ predictions: predictions.isEmpty ? nil : predictions,
|
|
|
+ carbsReq: 0,
|
|
|
+ temp: nil,
|
|
|
+ bg: 0,
|
|
|
+ reservoir: 0,
|
|
|
+ isf: 0,
|
|
|
+ tdd: 0,
|
|
|
+ insulin: nil,
|
|
|
+ current_target: 0,
|
|
|
+ insulinForManualBolus: 0,
|
|
|
+ manualBolusErrorString: 0,
|
|
|
+ minDelta: 0,
|
|
|
+ expectedDelta: 0,
|
|
|
+ minGuardBG: 0,
|
|
|
+ minPredBG: 0,
|
|
|
+ threshold: 0,
|
|
|
+ carbRatio: 0,
|
|
|
+ received: false
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @MainActor private func updateDeterminationsArray(with objects: [OrefDetermination]) {
|
|
|
+ guard let mostRecentDetermination = objects.first else { return }
|
|
|
+ determination = objects
|
|
|
+
|
|
|
+ // setup vars for bolus calculation
|
|
|
+ insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
|
|
|
+ evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
|
|
|
+ insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
|
|
|
+ target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
|
|
|
+ isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
|
|
|
+ cob = mostRecentDetermination.cob as Int16
|
|
|
+ iob = (mostRecentDetermination.iob ?? 0) as Decimal
|
|
|
+ basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
|
|
|
+ carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
|
|
|
+ insulinCalculated = calculateInsulin()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension Bolus.StateModel {
|
|
|
+ @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
|
|
|
+ if let forecastData = forecastData {
|
|
|
+ simulatedDetermination = forecastData
|
|
|
+ } else {
|
|
|
+ simulatedDetermination = await Task.detached { [self] in
|
|
|
+ await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
|
|
|
+ }.value
|
|
|
+ }
|
|
|
+
|
|
|
+ predictionsForChart = simulatedDetermination?.predictions
|
|
|
+
|
|
|
+ let nonEmptyArrays = [
|
|
|
+ predictionsForChart?.iob,
|
|
|
+ predictionsForChart?.zt,
|
|
|
+ predictionsForChart?.cob,
|
|
|
+ predictionsForChart?.uam
|
|
|
+ ]
|
|
|
+ .compactMap { $0 }
|
|
|
+ .filter { !$0.isEmpty }
|
|
|
+
|
|
|
+ guard !nonEmptyArrays.isEmpty else {
|
|
|
+ minForecast = []
|
|
|
+ maxForecast = []
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
|
|
|
+ guard minCount > 0 else { return }
|
|
|
+
|
|
|
+ let (minResult, maxResult) = await Task.detached {
|
|
|
+ let minForecast = (0 ..< self.minCount).map { index in
|
|
|
+ nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
|
|
|
+ }
|
|
|
+
|
|
|
+ let maxForecast = (0 ..< self.minCount).map { index in
|
|
|
+ nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
|
|
|
+ }
|
|
|
+
|
|
|
+ return (minForecast, maxForecast)
|
|
|
+ }.value
|
|
|
+
|
|
|
+ minForecast = minResult
|
|
|
+ maxForecast = maxResult
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private extension Set where Element == Forecast {
|
|
|
+ func extractValues(for type: String) -> [Int]? {
|
|
|
+ let values = first { $0.type == type }?
|
|
|
+ .forecastValues?
|
|
|
+ .sorted { $0.index < $1.index }
|
|
|
+ .compactMap { Int($0.value) }
|
|
|
+ return values?.isEmpty ?? true ? nil : values
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private extension Predictions {
|
|
|
+ var isEmpty: Bool {
|
|
|
+ iob == nil && zt == nil && cob == nil && uam == nil
|
|
|
+ }
|
|
|
}
|