BolusStateModel.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import LoopKit
  5. import Observation
  6. import SwiftUI
  7. import Swinject
  8. extension Bolus {
  9. @Observable final class StateModel: BaseStateModel<Provider> {
  10. @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
  11. @ObservationIgnored @Injected() var apsManager: APSManager!
  12. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  13. @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
  14. @ObservationIgnored @Injected() var settings: SettingsManager!
  15. @ObservationIgnored @Injected() var nsManager: NightscoutManager!
  16. @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
  17. @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
  18. @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
  19. var lowGlucose: Decimal = 70
  20. var highGlucose: Decimal = 180
  21. var glucoseColorScheme: GlucoseColorScheme = .staticColor
  22. var predictions: Predictions?
  23. var amount: Decimal = 0
  24. var insulinRecommended: Decimal = 0
  25. var insulinRequired: Decimal = 0
  26. var units: GlucoseUnits = .mgdL
  27. var threshold: Decimal = 0
  28. var maxBolus: Decimal = 0
  29. var maxExternal: Decimal { maxBolus * 3 }
  30. var errorString: Decimal = 0
  31. var evBG: Decimal = 0
  32. var insulin: Decimal = 0
  33. var isf: Decimal = 0
  34. var error: Bool = false
  35. var minGuardBG: Decimal = 0
  36. var minDelta: Decimal = 0
  37. var expectedDelta: Decimal = 0
  38. var minPredBG: Decimal = 0
  39. var waitForSuggestion: Bool = false
  40. var carbRatio: Decimal = 0
  41. var addButtonPressed: Bool = false
  42. var waitForSuggestionInitial: Bool = false
  43. var target: Decimal = 0
  44. var cob: Int16 = 0
  45. var iob: Decimal = 0
  46. var currentBG: Decimal = 0
  47. var fifteenMinInsulin: Decimal = 0
  48. var deltaBG: Decimal = 0
  49. var targetDifferenceInsulin: Decimal = 0
  50. var targetDifference: Decimal = 0
  51. var wholeCob: Decimal = 0
  52. var wholeCobInsulin: Decimal = 0
  53. var iobInsulinReduction: Decimal = 0
  54. var wholeCalc: Decimal = 0
  55. var insulinCalculated: Decimal = 0
  56. var fraction: Decimal = 0
  57. var basal: Decimal = 0
  58. var fattyMeals: Bool = false
  59. var fattyMealFactor: Decimal = 0
  60. var useFattyMealCorrectionFactor: Bool = false
  61. var displayPresets: Bool = true
  62. var currentBasal: Decimal = 0
  63. var currentCarbRatio: Decimal = 0
  64. var currentBGTarget: Decimal = 0
  65. var currentISF: Decimal = 0
  66. var sweetMeals: Bool = false
  67. var sweetMealFactor: Decimal = 0
  68. var useSuperBolus: Bool = false
  69. var superBolusInsulin: Decimal = 0
  70. var meal: [CarbsEntry]?
  71. var carbs: Decimal = 0
  72. var fat: Decimal = 0
  73. var protein: Decimal = 0
  74. var note: String = ""
  75. var date = Date()
  76. var carbsRequired: Decimal?
  77. var useFPUconversion: Bool = false
  78. var dish: String = ""
  79. var selection: MealPresetStored?
  80. var summation: [String] = []
  81. var maxCarbs: Decimal = 0
  82. var maxFat: Decimal = 0
  83. var maxProtein: Decimal = 0
  84. var id_: String = ""
  85. var summary: String = ""
  86. var externalInsulin: Bool = false
  87. var showInfo: Bool = false
  88. var glucoseFromPersistence: [GlucoseStored] = []
  89. var determination: [OrefDetermination] = []
  90. var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
  91. var predictionsForChart: Predictions?
  92. var simulatedDetermination: Determination?
  93. @MainActor var determinationObjectIDs: [NSManagedObjectID] = []
  94. var minForecast: [Int] = []
  95. var maxForecast: [Int] = []
  96. @MainActor var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
  97. var forecastDisplayType: ForecastDisplayType = .cone
  98. var isSmoothingEnabled: Bool = false
  99. var stops: [Gradient.Stop] = []
  100. let now = Date.now
  101. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  102. let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
  103. let determinationFetchContext = CoreDataStack.shared.newTaskContext()
  104. private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
  105. private var subscriptions = Set<AnyCancellable>()
  106. typealias PumpEvent = PumpEventStored.EventType
  107. override func subscribe() {
  108. coreDataPublisher =
  109. changedObjectsOnManagedObjectContextDidSavePublisher()
  110. .receive(on: DispatchQueue.global(qos: .background))
  111. .share()
  112. .eraseToAnyPublisher()
  113. registerHandlers()
  114. registerSubscribers()
  115. setupBolusStateConcurrently()
  116. }
  117. deinit {
  118. // Unregister from broadcaster
  119. broadcaster.unregister(DeterminationObserver.self, observer: self)
  120. broadcaster.unregister(BolusFailureObserver.self, observer: self)
  121. // Cancel Combine subscriptions
  122. subscriptions.forEach { $0.cancel() }
  123. subscriptions.removeAll()
  124. debugPrint("Bolus.StateModel deinitialized")
  125. }
  126. private func setupBolusStateConcurrently() {
  127. Task {
  128. await withTaskGroup(of: Void.self) { group in
  129. group.addTask {
  130. self.setupGlucoseArray()
  131. }
  132. group.addTask {
  133. self.setupDeterminationsAndForecasts()
  134. }
  135. group.addTask {
  136. await self.setupSettings()
  137. }
  138. group.addTask {
  139. self.registerObservers()
  140. }
  141. if self.waitForSuggestionInitial {
  142. group.addTask {
  143. let isDetermineBasalSuccessful = await self.apsManager.determineBasal()
  144. if !isDetermineBasalSuccessful {
  145. await MainActor.run {
  146. self.waitForSuggestion = false
  147. self.insulinRequired = 0
  148. self.insulinRecommended = 0
  149. }
  150. }
  151. }
  152. }
  153. }
  154. }
  155. }
  156. // MARK: - Basal
  157. private enum SettingType {
  158. case basal
  159. case carbRatio
  160. case bgTarget
  161. case isf
  162. }
  163. func getAllSettingsValues() async {
  164. await withTaskGroup(of: Void.self) { group in
  165. group.addTask {
  166. await self.getCurrentSettingValue(for: .basal)
  167. }
  168. group.addTask {
  169. await self.getCurrentSettingValue(for: .carbRatio)
  170. }
  171. group.addTask {
  172. await self.getCurrentSettingValue(for: .bgTarget)
  173. }
  174. group.addTask {
  175. await self.getCurrentSettingValue(for: .isf)
  176. }
  177. group.addTask {
  178. let getMaxBolus = await self.provider.getPumpSettings().maxBolus
  179. await MainActor.run {
  180. self.maxBolus = getMaxBolus
  181. }
  182. }
  183. }
  184. }
  185. private func setupDeterminationsAndForecasts() {
  186. Task {
  187. async let getAllSettingsDefaults: () = getAllSettingsValues()
  188. async let setupDeterminations: () = setupDeterminationsArray()
  189. await getAllSettingsDefaults
  190. await setupDeterminations
  191. // Determination has updated, so we can use this to draw the initial Forecast Chart
  192. let forecastData = await mapForecastsForChart()
  193. await updateForecasts(with: forecastData)
  194. }
  195. }
  196. private func registerObservers() {
  197. broadcaster.register(DeterminationObserver.self, observer: self)
  198. broadcaster.register(BolusFailureObserver.self, observer: self)
  199. }
  200. @MainActor private func setupSettings() async {
  201. units = settingsManager.settings.units
  202. fraction = settings.settings.overrideFactor
  203. fattyMeals = settings.settings.fattyMeals
  204. fattyMealFactor = settings.settings.fattyMealFactor
  205. sweetMeals = settings.settings.sweetMeals
  206. sweetMealFactor = settings.settings.sweetMealFactor
  207. displayPresets = settings.settings.displayPresets
  208. forecastDisplayType = settings.settings.forecastDisplayType
  209. lowGlucose = settingsManager.settings.low
  210. highGlucose = settingsManager.settings.high
  211. maxCarbs = settings.settings.maxCarbs
  212. maxFat = settings.settings.maxFat
  213. maxProtein = settings.settings.maxProtein
  214. useFPUconversion = settingsManager.settings.useFPUconversion
  215. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  216. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  217. }
  218. private func getCurrentSettingValue(for type: SettingType) async {
  219. let now = Date()
  220. let calendar = Calendar.current
  221. let dateFormatter = DateFormatter()
  222. dateFormatter.timeZone = TimeZone.current
  223. let regexWithSeconds = #"^\d{2}:\d{2}:\d{2}$"#
  224. let entries: [(start: String, value: Decimal)]
  225. switch type {
  226. case .basal:
  227. let basalEntries = await provider.getBasalProfile()
  228. entries = basalEntries.map { ($0.start, $0.rate) }
  229. case .carbRatio:
  230. let carbRatios = await provider.getCarbRatios()
  231. entries = carbRatios.schedule.map { ($0.start, $0.ratio) }
  232. case .bgTarget:
  233. let bgTargets = await provider.getBGTarget()
  234. entries = bgTargets.targets.map { ($0.start, $0.low) }
  235. case .isf:
  236. let isfValues = await provider.getISFValues()
  237. entries = isfValues.sensitivities.map { ($0.start, $0.sensitivity) }
  238. }
  239. for (index, entry) in entries.enumerated() {
  240. // Dynamically set the format based on whether it matches the regex
  241. if entry.start.range(of: regexWithSeconds, options: .regularExpression) != nil {
  242. dateFormatter.dateFormat = "HH:mm:ss"
  243. } else {
  244. dateFormatter.dateFormat = "HH:mm"
  245. }
  246. guard let entryTime = dateFormatter.date(from: entry.start) else {
  247. print("Invalid entry start time: \(entry.start)")
  248. continue
  249. }
  250. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  251. let entryStartTime = calendar.date(
  252. bySettingHour: entryComponents.hour!,
  253. minute: entryComponents.minute!,
  254. second: entryComponents.second ?? 0, // Set seconds to 0 if not provided
  255. of: now
  256. )!
  257. let entryEndTime: Date
  258. if index < entries.count - 1 {
  259. // Dynamically set the format again for the next element
  260. if entries[index + 1].start.range(of: regexWithSeconds, options: .regularExpression) != nil {
  261. dateFormatter.dateFormat = "HH:mm:ss"
  262. } else {
  263. dateFormatter.dateFormat = "HH:mm"
  264. }
  265. if let nextEntryTime = dateFormatter.date(from: entries[index + 1].start) {
  266. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  267. entryEndTime = calendar.date(
  268. bySettingHour: nextEntryComponents.hour!,
  269. minute: nextEntryComponents.minute!,
  270. second: nextEntryComponents.second ?? 0,
  271. of: now
  272. )!
  273. } else {
  274. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  275. }
  276. } else {
  277. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  278. }
  279. if now >= entryStartTime, now < entryEndTime {
  280. await MainActor.run {
  281. switch type {
  282. case .basal:
  283. currentBasal = entry.value
  284. case .carbRatio:
  285. currentCarbRatio = entry.value
  286. case .bgTarget:
  287. currentBGTarget = entry.value
  288. case .isf:
  289. currentISF = entry.value
  290. }
  291. }
  292. return
  293. }
  294. }
  295. }
  296. // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
  297. /// Calculate insulin recommendation
  298. func calculateInsulin() -> Decimal {
  299. let isfForCalculation = isf
  300. // insulin needed for the current blood glucose
  301. targetDifference = currentBG - target
  302. targetDifferenceInsulin = targetDifference / isfForCalculation
  303. // more or less insulin because of bg trend in the last 15 minutes
  304. fifteenMinInsulin = deltaBG / isfForCalculation
  305. // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
  306. wholeCob = Decimal(cob) + carbs
  307. wholeCobInsulin = wholeCob / carbRatio
  308. // determine how much the calculator reduces/ increases the bolus because of IOB
  309. iobInsulinReduction = (-1) * iob
  310. // adding everything together
  311. // add a calc for the case that no fifteenMinInsulin is available
  312. if deltaBG != 0 {
  313. wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin)
  314. } else {
  315. // add (rare) case that no glucose value is available -> maybe display warning?
  316. // if no bg is available, ?? sets its value to 0
  317. if currentBG == 0 {
  318. wholeCalc = (iobInsulinReduction + wholeCobInsulin)
  319. } else {
  320. wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin)
  321. }
  322. }
  323. // apply custom factor at the end of the calculations
  324. let result = wholeCalc * fraction
  325. // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
  326. if useFattyMealCorrectionFactor {
  327. insulinCalculated = result * fattyMealFactor
  328. } else if useSuperBolus {
  329. superBolusInsulin = sweetMealFactor * currentBasal
  330. insulinCalculated = result + superBolusInsulin
  331. } else {
  332. insulinCalculated = result
  333. }
  334. // display no negative insulinCalculated
  335. insulinCalculated = max(insulinCalculated, 0)
  336. insulinCalculated = min(insulinCalculated, maxBolus)
  337. guard let apsManager = apsManager else {
  338. return insulinCalculated
  339. }
  340. return apsManager.roundBolus(amount: insulinCalculated)
  341. }
  342. // MARK: - Button tasks
  343. func invokeTreatmentsTask() {
  344. Task {
  345. await MainActor.run {
  346. self.addButtonPressed = true
  347. }
  348. let isInsulinGiven = amount > 0
  349. let isCarbsPresent = carbs > 0
  350. let isFatPresent = fat > 0
  351. let isProteinPresent = protein > 0
  352. if isInsulinGiven {
  353. try await handleInsulin(isExternal: externalInsulin)
  354. } else if isCarbsPresent || isFatPresent || isProteinPresent {
  355. await MainActor.run {
  356. self.waitForSuggestion = true
  357. }
  358. } else {
  359. hideModal()
  360. return
  361. }
  362. await saveMeal()
  363. // If glucose data is stale end the custom loading animation by hiding the modal
  364. // Get date on Main thread
  365. let date = await MainActor.run {
  366. glucoseFromPersistence.first?.date
  367. }
  368. guard glucoseStorage.isGlucoseDataFresh(date) else {
  369. await MainActor.run {
  370. waitForSuggestion = false
  371. }
  372. return hideModal()
  373. }
  374. }
  375. }
  376. // MARK: - Insulin
  377. private func handleInsulin(isExternal: Bool) async throws {
  378. if !isExternal {
  379. await addPumpInsulin()
  380. } else {
  381. await addExternalInsulin()
  382. }
  383. await MainActor.run {
  384. self.waitForSuggestion = true
  385. }
  386. }
  387. func addPumpInsulin() async {
  388. guard amount > 0 else {
  389. showModal(for: nil)
  390. return
  391. }
  392. let maxAmount = Double(min(amount, maxBolus))
  393. do {
  394. let authenticated = try await unlockmanager.unlock()
  395. if authenticated {
  396. await apsManager.enactBolus(amount: maxAmount, isSMB: false)
  397. } else {
  398. print("authentication failed")
  399. }
  400. } catch {
  401. print("authentication error for pump bolus: \(error.localizedDescription)")
  402. await MainActor.run {
  403. self.waitForSuggestion = false
  404. if self.addButtonPressed {
  405. self.hideModal()
  406. }
  407. }
  408. }
  409. }
  410. // MARK: - EXTERNAL INSULIN
  411. func addExternalInsulin() async {
  412. guard amount > 0 else {
  413. showModal(for: nil)
  414. return
  415. }
  416. await MainActor.run {
  417. self.amount = min(self.amount, self.maxBolus * 3)
  418. }
  419. do {
  420. let authenticated = try await unlockmanager.unlock()
  421. if authenticated {
  422. // store external dose to pump history
  423. await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
  424. // perform determine basal sync
  425. await apsManager.determineBasalSync()
  426. } else {
  427. print("authentication failed")
  428. }
  429. } catch {
  430. print("authentication error for external insulin: \(error.localizedDescription)")
  431. await MainActor.run {
  432. self.waitForSuggestion = false
  433. if self.addButtonPressed {
  434. self.hideModal()
  435. }
  436. }
  437. }
  438. }
  439. // MARK: - Carbs
  440. func saveMeal() async {
  441. guard carbs > 0 || fat > 0 || protein > 0 else { return }
  442. await MainActor.run {
  443. self.carbs = min(self.carbs, self.maxCarbs)
  444. self.fat = min(self.fat, self.maxFat)
  445. self.protein = min(self.protein, self.maxProtein)
  446. self.id_ = UUID().uuidString
  447. }
  448. let carbsToStore = [CarbsEntry(
  449. id: id_,
  450. createdAt: now,
  451. actualDate: date,
  452. carbs: carbs,
  453. fat: fat,
  454. protein: protein,
  455. note: note,
  456. enteredBy: CarbsEntry.manual,
  457. isFPU: false, fpuID: UUID().uuidString
  458. )]
  459. await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
  460. if carbs > 0 || fat > 0 || protein > 0 {
  461. // 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
  462. if amount <= 0 {
  463. await apsManager.determineBasalSync()
  464. }
  465. }
  466. }
  467. // MARK: - Presets
  468. func deletePreset() {
  469. if selection != nil {
  470. viewContext.delete(selection!)
  471. do {
  472. guard viewContext.hasChanges else { return }
  473. try viewContext.save()
  474. } catch {
  475. print(error.localizedDescription)
  476. }
  477. carbs = 0
  478. fat = 0
  479. protein = 0
  480. }
  481. selection = nil
  482. }
  483. func removePresetFromNewMeal() {
  484. let a = summation.firstIndex(where: { $0 == selection?.dish! })
  485. if a != nil, summation[a ?? 0] != "" {
  486. summation.remove(at: a!)
  487. }
  488. }
  489. func addPresetToNewMeal() {
  490. let test: String = selection?.dish ?? "dontAdd"
  491. if test != "dontAdd" {
  492. summation.append(test)
  493. }
  494. }
  495. func addNewPresetToWaitersNotepad(_ dish: String) {
  496. summation.append(dish)
  497. }
  498. func addToSummation() {
  499. summation.append(selection?.dish ?? "")
  500. }
  501. }
  502. }
  503. extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
  504. func determinationDidUpdate(_: Determination) {
  505. DispatchQueue.main.async {
  506. self.waitForSuggestion = false
  507. if self.addButtonPressed {
  508. self.hideModal()
  509. }
  510. }
  511. }
  512. func bolusDidFail() {
  513. DispatchQueue.main.async {
  514. self.waitForSuggestion = false
  515. if self.addButtonPressed {
  516. self.hideModal()
  517. }
  518. }
  519. }
  520. }
  521. extension Bolus.StateModel {
  522. private func registerHandlers() {
  523. coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
  524. guard let self = self else { return }
  525. Task {
  526. await self.setupDeterminationsArray()
  527. let forecastData = await self.mapForecastsForChart()
  528. await self.updateForecasts(with: forecastData)
  529. }
  530. }.store(in: &subscriptions)
  531. // Due to the Batch insert this only is used for observing Deletion of Glucose entries
  532. coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
  533. guard let self = self else { return }
  534. self.setupGlucoseArray()
  535. }.store(in: &subscriptions)
  536. }
  537. private func registerSubscribers() {
  538. glucoseStorage.updatePublisher
  539. .receive(on: DispatchQueue.global(qos: .background))
  540. .sink { [weak self] _ in
  541. guard let self = self else { return }
  542. self.setupGlucoseArray()
  543. }
  544. .store(in: &subscriptions)
  545. }
  546. }
  547. // MARK: - Setup Glucose and Determinations
  548. extension Bolus.StateModel {
  549. // Glucose
  550. private func setupGlucoseArray() {
  551. Task {
  552. let ids = await self.fetchGlucose()
  553. let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  554. await updateGlucoseArray(with: glucoseObjects)
  555. }
  556. }
  557. private func fetchGlucose() async -> [NSManagedObjectID] {
  558. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  559. ofType: GlucoseStored.self,
  560. onContext: glucoseFetchContext,
  561. predicate: NSPredicate.glucose,
  562. key: "date",
  563. ascending: false,
  564. fetchLimit: 288
  565. )
  566. return await glucoseFetchContext.perform {
  567. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  568. return fetchedResults.map(\.objectID)
  569. }
  570. }
  571. @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
  572. glucoseFromPersistence = objects
  573. let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
  574. let thirdLastGlucose = glucoseFromPersistence.dropFirst(2).first?.glucose ?? 0
  575. let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
  576. currentBG = Decimal(lastGlucose)
  577. deltaBG = delta
  578. }
  579. // Determinations
  580. private func setupDeterminationsArray() async {
  581. // Fetch object IDs on a background thread
  582. let fetchedObjectIDs = await determinationStorage.fetchLastDeterminationObjectID(
  583. predicate: NSPredicate.predicateFor30MinAgoForDetermination
  584. )
  585. // Update determinationObjectIDs on the main thread
  586. await MainActor.run {
  587. determinationObjectIDs = fetchedObjectIDs
  588. }
  589. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  590. .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
  591. await updateDeterminationsArray(with: determinationObjects)
  592. }
  593. private func mapForecastsForChart() async -> Determination? {
  594. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  595. .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
  596. return await determinationFetchContext.perform {
  597. guard let determinationObject = determinationObjects.first else {
  598. return nil
  599. }
  600. let eventualBG = determinationObject.eventualBG?.intValue
  601. let forecastsSet = determinationObject.forecasts ?? []
  602. let predictions = Predictions(
  603. iob: forecastsSet.extractValues(for: "iob"),
  604. zt: forecastsSet.extractValues(for: "zt"),
  605. cob: forecastsSet.extractValues(for: "cob"),
  606. uam: forecastsSet.extractValues(for: "uam")
  607. )
  608. return Determination(
  609. id: UUID(),
  610. reason: "",
  611. units: 0,
  612. insulinReq: 0,
  613. eventualBG: eventualBG,
  614. sensitivityRatio: 0,
  615. rate: 0,
  616. duration: 0,
  617. iob: 0,
  618. cob: 0,
  619. predictions: predictions.isEmpty ? nil : predictions,
  620. carbsReq: 0,
  621. temp: nil,
  622. bg: 0,
  623. reservoir: 0,
  624. isf: 0,
  625. tdd: 0,
  626. insulin: nil,
  627. current_target: 0,
  628. insulinForManualBolus: 0,
  629. manualBolusErrorString: 0,
  630. minDelta: 0,
  631. expectedDelta: 0,
  632. minGuardBG: 0,
  633. minPredBG: 0,
  634. threshold: 0,
  635. carbRatio: 0,
  636. received: false
  637. )
  638. }
  639. }
  640. @MainActor private func updateDeterminationsArray(with objects: [OrefDetermination]) {
  641. guard let mostRecentDetermination = objects.first else { return }
  642. determination = objects
  643. // setup vars for bolus calculation
  644. insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
  645. evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
  646. insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
  647. target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
  648. isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
  649. cob = mostRecentDetermination.cob as Int16
  650. iob = (mostRecentDetermination.iob ?? 0) as Decimal
  651. basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
  652. carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
  653. insulinCalculated = calculateInsulin()
  654. }
  655. }
  656. extension Bolus.StateModel {
  657. @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
  658. if let forecastData = forecastData {
  659. simulatedDetermination = forecastData
  660. } else {
  661. simulatedDetermination = await Task { [self] in
  662. await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
  663. }.value
  664. }
  665. predictionsForChart = simulatedDetermination?.predictions
  666. let nonEmptyArrays = [
  667. predictionsForChart?.iob,
  668. predictionsForChart?.zt,
  669. predictionsForChart?.cob,
  670. predictionsForChart?.uam
  671. ]
  672. .compactMap { $0 }
  673. .filter { !$0.isEmpty }
  674. guard !nonEmptyArrays.isEmpty else {
  675. minForecast = []
  676. maxForecast = []
  677. return
  678. }
  679. minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
  680. guard minCount > 0 else { return }
  681. async let minForecastResult = Task {
  682. await (0 ..< self.minCount).map { index in
  683. nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
  684. }
  685. }.value
  686. async let maxForecastResult = Task {
  687. await (0 ..< self.minCount).map { index in
  688. nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
  689. }
  690. }.value
  691. minForecast = await minForecastResult
  692. maxForecast = await maxForecastResult
  693. }
  694. }
  695. private extension Set where Element == Forecast {
  696. func extractValues(for type: String) -> [Int]? {
  697. let values = first { $0.type == type }?
  698. .forecastValues?
  699. .sorted { $0.index < $1.index }
  700. .compactMap { Int($0.value) }
  701. return values?.isEmpty ?? true ? nil : values
  702. }
  703. }
  704. private extension Predictions {
  705. var isEmpty: Bool {
  706. iob == nil && zt == nil && cob == nil && uam == nil
  707. }
  708. }