HomeStateModel.swift 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import LoopKitUI
  5. import SwiftDate
  6. import SwiftUI
  7. extension Home {
  8. final class StateModel: BaseStateModel<Provider> {
  9. @Injected() var broadcaster: Broadcaster!
  10. @Injected() var apsManager: APSManager!
  11. @Injected() var fetchGlucoseManager: FetchGlucoseManager!
  12. @Injected() var nightscoutManager: NightscoutManager!
  13. @Injected() var determinationStorage: DeterminationStorage!
  14. @Injected() var glucoseStorage: GlucoseStorage!
  15. private let timer = DispatchTimer(timeInterval: 5)
  16. private(set) var filteredHours = 24
  17. @Published var manualGlucose: [BloodGlucose] = []
  18. @Published var announcement: [Announcement] = []
  19. @Published var uploadStats = false
  20. @Published var recentGlucose: BloodGlucose?
  21. @Published var maxBasal: Decimal = 2
  22. @Published var autotunedBasalProfile: [BasalProfileEntry] = []
  23. @Published var basalProfile: [BasalProfileEntry] = []
  24. @Published var tempTargets: [TempTarget] = []
  25. @Published var timerDate = Date()
  26. @Published var closedLoop = false
  27. @Published var pumpSuspended = false
  28. @Published var isLooping = false
  29. @Published var statusTitle = ""
  30. @Published var lastLoopDate: Date = .distantPast
  31. @Published var battery: Battery?
  32. @Published var reservoir: Decimal?
  33. @Published var pumpName = ""
  34. @Published var pumpExpiresAtDate: Date?
  35. @Published var tempTarget: TempTarget?
  36. @Published var setupPump = false
  37. @Published var errorMessage: String? = nil
  38. @Published var errorDate: Date? = nil
  39. @Published var bolusProgress: Decimal?
  40. @Published var eventualBG: Int?
  41. @Published var allowManualTemp = false
  42. @Published var units: GlucoseUnits = .mgdL
  43. @Published var pumpDisplayState: PumpDisplayState?
  44. @Published var alarm: GlucoseAlarm?
  45. @Published var animatedBackground = false
  46. @Published var manualTempBasal = false
  47. @Published var smooth = false
  48. @Published var maxValue: Decimal = 1.2
  49. @Published var lowGlucose: Decimal = 4 / 0.0555
  50. @Published var highGlucose: Decimal = 10 / 0.0555
  51. @Published var overrideUnit: Bool = false
  52. @Published var displayXgridLines: Bool = false
  53. @Published var displayYgridLines: Bool = false
  54. @Published var thresholdLines: Bool = false
  55. @Published var timeZone: TimeZone?
  56. @Published var hours: Int16 = 6
  57. @Published var totalBolus: Decimal = 0
  58. @Published var isStatusPopupPresented: Bool = false
  59. @Published var isLegendPresented: Bool = false
  60. @Published var legendSheetDetent = PresentationDetent.large
  61. @Published var tins: Bool = false
  62. @Published var isTempTargetActive: Bool = false
  63. @Published var roundedTotalBolus: String = ""
  64. @Published var selectedTab: Int = 0
  65. @Published var waitForSuggestion: Bool = false
  66. @Published var glucoseFromPersistence: [GlucoseStored] = []
  67. @Published var manualGlucoseFromPersistence: [GlucoseStored] = []
  68. @Published var carbsFromPersistence: [CarbEntryStored] = []
  69. @Published var fpusFromPersistence: [CarbEntryStored] = []
  70. @Published var determinationsFromPersistence: [OrefDetermination] = []
  71. @Published var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
  72. @Published var insulinFromPersistence: [PumpEventStored] = []
  73. @Published var tempBasals: [PumpEventStored] = []
  74. @Published var suspensions: [PumpEventStored] = []
  75. @Published var batteryFromPersistence: [OpenAPS_Battery] = []
  76. @Published var lastPumpBolus: PumpEventStored?
  77. @Published var overrides: [OverrideStored] = []
  78. @Published var overrideRunStored: [OverrideRunStored] = []
  79. @Published var isOverrideCancelled: Bool = false
  80. @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
  81. @Published var pumpStatusHighlightMessage: String? = nil
  82. @Published var cgmAvailable: Bool = false
  83. @Published var minForecast: [Int] = []
  84. @Published var maxForecast: [Int] = []
  85. @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
  86. @Published var displayForecastsAsLines: Bool = false
  87. let context = CoreDataStack.shared.newTaskContext()
  88. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  89. private var coreDataObserver: CoreDataObserver?
  90. typealias PumpEvent = PumpEventStored.EventType
  91. override func subscribe() {
  92. setupNotification()
  93. coreDataObserver = CoreDataObserver()
  94. registerHandlers()
  95. setupGlucoseArray()
  96. setupManualGlucoseArray()
  97. setupCarbsArray()
  98. setupFPUsArray()
  99. setupDeterminationsArray()
  100. setupInsulinArray()
  101. setupLastBolus()
  102. setupBatteryArray()
  103. setupPumpSettings()
  104. setupBasalProfile()
  105. setupTempTargets()
  106. setupReservoir()
  107. setupAnnouncements()
  108. setupCurrentPumpTimezone()
  109. setupOverrides()
  110. setupOverrideRunStored()
  111. // TODO: isUploadEnabled the right var here??
  112. uploadStats = settingsManager.settings.isUploadEnabled
  113. units = settingsManager.settings.units
  114. allowManualTemp = !settingsManager.settings.closedLoop
  115. closedLoop = settingsManager.settings.closedLoop
  116. lastLoopDate = apsManager.lastLoopDate
  117. alarm = provider.glucoseStorage.alarm
  118. manualTempBasal = apsManager.isManualTempBasal
  119. setupCurrentTempTarget()
  120. smooth = settingsManager.settings.smoothGlucose
  121. maxValue = settingsManager.preferences.autosensMax
  122. lowGlucose = settingsManager.settings.low
  123. highGlucose = settingsManager.settings.high
  124. overrideUnit = settingsManager.settings.overrideHbA1cUnit
  125. displayXgridLines = settingsManager.settings.xGridLines
  126. displayYgridLines = settingsManager.settings.yGridLines
  127. thresholdLines = settingsManager.settings.rulerMarks
  128. tins = settingsManager.settings.tins
  129. cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
  130. displayForecastsAsLines = settingsManager.settings.displayForecastsAsLines
  131. broadcaster.register(GlucoseObserver.self, observer: self)
  132. broadcaster.register(DeterminationObserver.self, observer: self)
  133. broadcaster.register(SettingsObserver.self, observer: self)
  134. broadcaster.register(PumpSettingsObserver.self, observer: self)
  135. broadcaster.register(BasalProfileObserver.self, observer: self)
  136. broadcaster.register(TempTargetsObserver.self, observer: self)
  137. broadcaster.register(PumpReservoirObserver.self, observer: self)
  138. broadcaster.register(PumpDeactivatedObserver.self, observer: self)
  139. animatedBackground = settingsManager.settings.animatedBackground
  140. timer.eventHandler = {
  141. DispatchQueue.main.async { [weak self] in
  142. self?.timerDate = Date()
  143. self?.setupCurrentTempTarget()
  144. }
  145. }
  146. timer.resume()
  147. apsManager.isLooping
  148. .receive(on: DispatchQueue.main)
  149. .weakAssign(to: \.isLooping, on: self)
  150. .store(in: &lifetime)
  151. apsManager.lastLoopDateSubject
  152. .receive(on: DispatchQueue.main)
  153. .weakAssign(to: \.lastLoopDate, on: self)
  154. .store(in: &lifetime)
  155. apsManager.pumpName
  156. .receive(on: DispatchQueue.main)
  157. .weakAssign(to: \.pumpName, on: self)
  158. .store(in: &lifetime)
  159. apsManager.pumpExpiresAtDate
  160. .receive(on: DispatchQueue.main)
  161. .weakAssign(to: \.pumpExpiresAtDate, on: self)
  162. .store(in: &lifetime)
  163. apsManager.lastError
  164. .receive(on: DispatchQueue.main)
  165. .map { [weak self] error in
  166. self?.errorDate = error == nil ? nil : Date()
  167. if let error = error {
  168. info(.default, error.localizedDescription)
  169. }
  170. return error?.localizedDescription
  171. }
  172. .weakAssign(to: \.errorMessage, on: self)
  173. .store(in: &lifetime)
  174. apsManager.bolusProgress
  175. .receive(on: DispatchQueue.main)
  176. .weakAssign(to: \.bolusProgress, on: self)
  177. .store(in: &lifetime)
  178. apsManager.pumpDisplayState
  179. .receive(on: DispatchQueue.main)
  180. .sink { [weak self] state in
  181. guard let self = self else { return }
  182. self.pumpDisplayState = state
  183. if state == nil {
  184. self.reservoir = nil
  185. self.battery = nil
  186. self.pumpName = ""
  187. self.pumpExpiresAtDate = nil
  188. self.setupPump = false
  189. } else {
  190. self.setupReservoir()
  191. self.displayPumpStatusHighlightMessage()
  192. self.setupBatteryArray()
  193. }
  194. }
  195. .store(in: &lifetime)
  196. $setupPump
  197. .sink { [weak self] show in
  198. guard let self = self else { return }
  199. if show, let pumpManager = self.provider.apsManager.pumpManager,
  200. let bluetoothProvider = self.provider.apsManager.bluetoothManager
  201. {
  202. let view = PumpConfig.PumpSettingsView(
  203. pumpManager: pumpManager,
  204. bluetoothManager: bluetoothProvider,
  205. completionDelegate: self,
  206. setupDelegate: self
  207. ).asAny()
  208. self.router.mainSecondaryModalView.send(view)
  209. } else if show {
  210. self.router.mainSecondaryModalView.send(self.router.view(for: .pumpConfigDirect))
  211. } else {
  212. self.router.mainSecondaryModalView.send(nil)
  213. }
  214. }
  215. .store(in: &lifetime)
  216. }
  217. private func registerHandlers() {
  218. coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
  219. guard let self = self else { return }
  220. self.setupDeterminationsArray()
  221. }
  222. coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
  223. guard let self = self else { return }
  224. self.setupGlucoseArray()
  225. self.setupManualGlucoseArray()
  226. }
  227. coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
  228. guard let self = self else { return }
  229. self.setupCarbsArray()
  230. }
  231. coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
  232. guard let self = self else { return }
  233. self.setupInsulinArray()
  234. self.setupLastBolus()
  235. self.displayPumpStatusHighlightMessage()
  236. }
  237. coreDataObserver?.registerHandler(for: "OpenAPS_Battery") { [weak self] in
  238. guard let self = self else { return }
  239. self.setupBatteryArray()
  240. }
  241. coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
  242. guard let self = self else { return }
  243. self.setupOverrides()
  244. }
  245. coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
  246. guard let self = self else { return }
  247. self.setupOverrideRunStored()
  248. }
  249. }
  250. /// Display the eventual status message provided by the manager of the pump
  251. /// Only display if state is warning or critical message else return nil
  252. private func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
  253. DispatchQueue.main.async { [weak self] in
  254. guard let self = self else { return }
  255. if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
  256. statusHighlight.state == .warning || statusHighlight.state == .critical, !didDeactivate
  257. {
  258. pumpStatusHighlightMessage = (statusHighlight.state == .warning ? "⚠️\n" : "‼️\n") + statusHighlight
  259. .localizedMessage
  260. } else {
  261. pumpStatusHighlightMessage = nil
  262. }
  263. }
  264. }
  265. func runLoop() {
  266. provider.heartbeatNow()
  267. }
  268. func showProgressView() {
  269. glucoseStorage
  270. .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
  271. }
  272. func cancelBolus() {
  273. Task {
  274. await apsManager.cancelBolus()
  275. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  276. await apsManager.determineBasalSync()
  277. }
  278. }
  279. @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
  280. do {
  281. let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
  282. profileToCancel?.enabled = false
  283. await saveToOverrideRunStored(withID: id)
  284. guard viewContext.hasChanges else { return }
  285. try viewContext.save()
  286. Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
  287. } catch {
  288. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
  289. }
  290. }
  291. func calculateTINS() -> String {
  292. let startTime = calculateStartTime(hours: Int(hours))
  293. let totalBolus = calculateTotalBolus(from: insulinFromPersistence, since: startTime)
  294. let totalBasal = calculateTotalBasal(from: insulinFromPersistence, since: startTime)
  295. let totalInsulin = totalBolus + totalBasal
  296. return formatInsulinAmount(totalInsulin)
  297. }
  298. private func calculateStartTime(hours: Int) -> Date {
  299. let date = Date()
  300. let calendar = Calendar.current
  301. var offsetComponents = DateComponents()
  302. offsetComponents.hour = -hours
  303. return calendar.date(byAdding: offsetComponents, to: date)!
  304. }
  305. private func calculateTotalBolus(from events: [PumpEventStored], since startTime: Date) -> Double {
  306. let bolusEvents = events.filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.bolus.rawValue }
  307. return bolusEvents.compactMap { $0.bolus?.amount?.doubleValue }.reduce(0, +)
  308. }
  309. private func calculateTotalBasal(from events: [PumpEventStored], since startTime: Date) -> Double {
  310. let basalEvents = events
  311. .filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.tempBasal.rawValue }
  312. .sorted { $0.timestamp ?? .distantPast < $1.timestamp ?? .distantPast }
  313. var basalDurations: [Double] = []
  314. for (index, basalEntry) in basalEvents.enumerated() {
  315. if index + 1 < basalEvents.count {
  316. let nextEntry = basalEvents[index + 1]
  317. let durationInSeconds = nextEntry.timestamp?.timeIntervalSince(basalEntry.timestamp ?? Date()) ?? 0
  318. basalDurations.append(durationInSeconds / 3600) // Conversion to hours
  319. }
  320. }
  321. return zip(basalEvents, basalDurations).map { entry, duration in
  322. guard let rate = entry.tempBasal?.rate?.doubleValue else { return 0 }
  323. return rate * duration
  324. }.reduce(0, +)
  325. }
  326. private func formatInsulinAmount(_ amount: Double) -> String {
  327. let roundedAmount = Decimal(round(100 * amount) / 100)
  328. return roundedAmount.formatted()
  329. }
  330. private func setupPumpSettings() {
  331. DispatchQueue.main.async { [weak self] in
  332. guard let self = self else { return }
  333. self.maxBasal = self.provider.pumpSettings().maxBasal
  334. }
  335. }
  336. private func setupBasalProfile() {
  337. DispatchQueue.main.async { [weak self] in
  338. guard let self = self else { return }
  339. self.autotunedBasalProfile = self.provider.autotunedBasalProfile()
  340. self.basalProfile = self.provider.basalProfile()
  341. }
  342. }
  343. private func setupTempTargets() {
  344. DispatchQueue.main.async { [weak self] in
  345. guard let self = self else { return }
  346. self.manualTempBasal = self.apsManager.isManualTempBasal
  347. self.tempTargets = self.provider.tempTargets(hours: self.filteredHours)
  348. }
  349. }
  350. private func setupAnnouncements() {
  351. DispatchQueue.main.async { [weak self] in
  352. guard let self = self else { return }
  353. self.announcement = self.provider.announcement(self.filteredHours)
  354. }
  355. }
  356. private func setupReservoir() {
  357. DispatchQueue.main.async { [weak self] in
  358. guard let self = self else { return }
  359. self.reservoir = self.provider.pumpReservoir()
  360. }
  361. }
  362. private func setupCurrentTempTarget() {
  363. tempTarget = provider.tempTarget()
  364. }
  365. private func setupCurrentPumpTimezone() {
  366. DispatchQueue.main.async { [weak self] in
  367. guard let self = self else { return }
  368. self.timeZone = self.provider.pumpTimeZone()
  369. }
  370. }
  371. func openCGM() {
  372. router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
  373. }
  374. func infoPanelTTPercentage(_ hbt_: Double, _ target: Decimal) -> Decimal {
  375. guard hbt_ != 0 || target != 0 else {
  376. return 0
  377. }
  378. let c = Decimal(hbt_ - 100)
  379. let ratio = min(c / (target + c - 100), maxValue)
  380. return (ratio * 100)
  381. }
  382. }
  383. }
  384. extension Home.StateModel:
  385. GlucoseObserver,
  386. DeterminationObserver,
  387. SettingsObserver,
  388. PumpSettingsObserver,
  389. BasalProfileObserver,
  390. TempTargetsObserver,
  391. PumpReservoirObserver,
  392. PumpTimeZoneObserver,
  393. PumpDeactivatedObserver
  394. {
  395. // TODO: still needed?
  396. func glucoseDidUpdate(_: [BloodGlucose]) {
  397. // setupGlucose()
  398. }
  399. func determinationDidUpdate(_: Determination) {
  400. waitForSuggestion = false
  401. }
  402. func settingsDidChange(_ settings: FreeAPSSettings) {
  403. allowManualTemp = !settings.closedLoop
  404. closedLoop = settingsManager.settings.closedLoop
  405. units = settingsManager.settings.units
  406. animatedBackground = settingsManager.settings.animatedBackground
  407. manualTempBasal = apsManager.isManualTempBasal
  408. smooth = settingsManager.settings.smoothGlucose
  409. lowGlucose = settingsManager.settings.low
  410. highGlucose = settingsManager.settings.high
  411. overrideUnit = settingsManager.settings.overrideHbA1cUnit
  412. displayXgridLines = settingsManager.settings.xGridLines
  413. displayYgridLines = settingsManager.settings.yGridLines
  414. thresholdLines = settingsManager.settings.rulerMarks
  415. displayForecastsAsLines = settingsManager.settings.displayForecastsAsLines
  416. tins = settingsManager.settings.tins
  417. cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
  418. displayPumpStatusHighlightMessage()
  419. setupBatteryArray()
  420. }
  421. // TODO: is this ever really triggered? react to MOC changes?
  422. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  423. displayPumpStatusHighlightMessage()
  424. }
  425. func pumpSettingsDidChange(_: PumpSettings) {
  426. setupPumpSettings()
  427. setupBatteryArray()
  428. }
  429. func basalProfileDidChange(_: [BasalProfileEntry]) {
  430. setupBasalProfile()
  431. }
  432. func tempTargetsDidUpdate(_: [TempTarget]) {
  433. setupTempTargets()
  434. }
  435. func pumpReservoirDidChange(_: Decimal) {
  436. setupReservoir()
  437. displayPumpStatusHighlightMessage()
  438. }
  439. func pumpDeactivatedDidChange() {
  440. displayPumpStatusHighlightMessage(true)
  441. batteryFromPersistence = []
  442. }
  443. func pumpTimeZoneDidChange(_: TimeZone) {
  444. setupCurrentPumpTimezone()
  445. }
  446. }
  447. extension Home.StateModel: CompletionDelegate {
  448. func completionNotifyingDidComplete(_: CompletionNotifying) {
  449. setupPump = false
  450. }
  451. }
  452. extension Home.StateModel: PumpManagerOnboardingDelegate {
  453. func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
  454. provider.apsManager.pumpManager = pumpManager
  455. if let insulinType = pumpManager.status.insulinType {
  456. settingsManager.updateInsulinCurve(insulinType)
  457. }
  458. }
  459. func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
  460. // nothing to do
  461. }
  462. func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
  463. // TODO:
  464. }
  465. }
  466. // MARK: - Setup Core Data observation
  467. extension Home.StateModel {
  468. /// listens for the notifications sent when the managedObjectContext has saved!
  469. func setupNotification() {
  470. /// custom notification that is sent when a batch insert of glucose objects is done
  471. Foundation.NotificationCenter.default.addObserver(
  472. self,
  473. selector: #selector(handleBatchInsert),
  474. name: .didPerformBatchInsert,
  475. object: nil
  476. )
  477. /// custom notification that is sent when a batch delete of fpus is done
  478. Foundation.NotificationCenter.default.addObserver(
  479. self,
  480. selector: #selector(handleBatchDelete),
  481. name: .didPerformBatchDelete,
  482. object: nil
  483. )
  484. }
  485. @objc private func handleBatchInsert() {
  486. setupFPUsArray()
  487. setupGlucoseArray()
  488. }
  489. @objc private func handleBatchDelete() {
  490. setupFPUsArray()
  491. }
  492. }
  493. // MARK: - Handle Core Data changes and update Arrays to display them in the UI
  494. extension Home.StateModel {
  495. // Setup Glucose
  496. private func setupGlucoseArray() {
  497. Task {
  498. let ids = await self.fetchGlucose()
  499. let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  500. await updateGlucoseArray(with: glucoseObjects)
  501. }
  502. }
  503. private func fetchGlucose() async -> [NSManagedObjectID] {
  504. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  505. ofType: GlucoseStored.self,
  506. onContext: context,
  507. predicate: NSPredicate.glucose,
  508. key: "date",
  509. ascending: false,
  510. fetchLimit: 288
  511. )
  512. return await context.perform {
  513. return results.map(\.objectID)
  514. }
  515. }
  516. @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
  517. glucoseFromPersistence = objects
  518. }
  519. // Setup Manual Glucose
  520. private func setupManualGlucoseArray() {
  521. Task {
  522. let ids = await self.fetchManualGlucose()
  523. let manualGlucoseObjects: [GlucoseStored] = await CoreDataStack.shared
  524. .getNSManagedObject(with: ids, context: viewContext)
  525. await updateManualGlucoseArray(with: manualGlucoseObjects)
  526. }
  527. }
  528. private func fetchManualGlucose() async -> [NSManagedObjectID] {
  529. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  530. ofType: GlucoseStored.self,
  531. onContext: context,
  532. predicate: NSPredicate.manualGlucose,
  533. key: "date",
  534. ascending: false,
  535. fetchLimit: 288
  536. )
  537. return await context.perform {
  538. return results.map(\.objectID)
  539. }
  540. }
  541. @MainActor private func updateManualGlucoseArray(with objects: [GlucoseStored]) {
  542. manualGlucoseFromPersistence = objects
  543. }
  544. // Setup Carbs
  545. private func setupCarbsArray() {
  546. Task {
  547. let ids = await self.fetchCarbs()
  548. let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  549. await updateCarbsArray(with: carbObjects)
  550. }
  551. }
  552. private func fetchCarbs() async -> [NSManagedObjectID] {
  553. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  554. ofType: CarbEntryStored.self,
  555. onContext: context,
  556. predicate: NSPredicate.carbsForChart,
  557. key: "date",
  558. ascending: false
  559. )
  560. return await context.perform {
  561. return results.map(\.objectID)
  562. }
  563. }
  564. @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
  565. carbsFromPersistence = objects
  566. }
  567. // Setup FPUs
  568. private func setupFPUsArray() {
  569. Task {
  570. let ids = await self.fetchFPUs()
  571. let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  572. await updateFPUsArray(with: fpuObjects)
  573. }
  574. }
  575. private func fetchFPUs() async -> [NSManagedObjectID] {
  576. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  577. ofType: CarbEntryStored.self,
  578. onContext: context,
  579. predicate: NSPredicate.fpusForChart,
  580. key: "date",
  581. ascending: false
  582. )
  583. return await context.perform {
  584. return results.map(\.objectID)
  585. }
  586. }
  587. @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
  588. fpusFromPersistence = objects
  589. }
  590. // Custom fetch to more efficiently filter only for cob and iob
  591. private func fetchCobAndIob() async -> [NSManagedObjectID] {
  592. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  593. ofType: OrefDetermination.self,
  594. onContext: context,
  595. predicate: NSPredicate.determinationsForCobIobCharts,
  596. key: "deliverAt",
  597. ascending: false,
  598. batchSize: 50,
  599. propertiesToFetch: ["cob", "iob", "deliverAt"]
  600. )
  601. return await context.perform {
  602. return results.map(\.objectID)
  603. }
  604. }
  605. // Setup Determinations
  606. private func setupDeterminationsArray() {
  607. Task {
  608. // Get the NSManagedObjectIDs
  609. async let enactedObjectIDs = determinationStorage
  610. .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
  611. async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
  612. let enactedIDs = await enactedObjectIDs
  613. let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
  614. // Get the NSManagedObjects and return them on the Main Thread
  615. await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
  616. await updateDeterminationsArray(with: enactedAndNonEnactedIDs, keyPath: \.enactedAndNonEnactedDeterminations)
  617. await updateForecastData()
  618. }
  619. }
  620. @MainActor private func updateDeterminationsArray(
  621. with IDs: [NSManagedObjectID],
  622. keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
  623. ) async {
  624. // Fetch the objects off the main thread
  625. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  626. .getNSManagedObject(with: IDs, context: viewContext)
  627. // Update the array on the main thread
  628. self[keyPath: keyPath] = determinationObjects
  629. }
  630. // Setup Insulin
  631. private func setupInsulinArray() {
  632. Task {
  633. let ids = await self.fetchInsulin()
  634. let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  635. await updateInsulinArray(with: insulinObjects)
  636. }
  637. }
  638. private func fetchInsulin() async -> [NSManagedObjectID] {
  639. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  640. ofType: PumpEventStored.self,
  641. onContext: context,
  642. predicate: NSPredicate.pumpHistoryLast24h,
  643. key: "timestamp",
  644. ascending: true
  645. )
  646. return await context.perform {
  647. return results.map(\.objectID)
  648. }
  649. }
  650. @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
  651. insulinFromPersistence = insulinObjects
  652. // Filter tempbasals
  653. manualTempBasal = apsManager.isManualTempBasal
  654. tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
  655. // Suspension and resume events
  656. suspensions = insulinFromPersistence.filter {
  657. $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
  658. }
  659. let lastSuspension = suspensions.last
  660. pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
  661. .type == EventType.pumpSuspend.rawValue
  662. }
  663. // Setup Last Bolus to display the bolus progress bar
  664. // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
  665. private func setupLastBolus() {
  666. Task {
  667. guard let id = await self.fetchLastBolus() else { return }
  668. await updateLastBolus(with: id)
  669. }
  670. }
  671. private func fetchLastBolus() async -> NSManagedObjectID? {
  672. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  673. ofType: PumpEventStored.self,
  674. onContext: context,
  675. predicate: NSPredicate.lastPumpBolus,
  676. key: "timestamp",
  677. ascending: false,
  678. fetchLimit: 1
  679. )
  680. return await context.perform {
  681. return results.map(\.objectID).first
  682. }
  683. }
  684. @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
  685. do {
  686. lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
  687. } catch {
  688. debugPrint(
  689. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
  690. )
  691. }
  692. }
  693. // Setup Battery
  694. private func setupBatteryArray() {
  695. Task {
  696. let ids = await self.fetchBattery()
  697. let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  698. await updateBatteryArray(with: batteryObjects)
  699. }
  700. }
  701. private func fetchBattery() async -> [NSManagedObjectID] {
  702. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  703. ofType: OpenAPS_Battery.self,
  704. onContext: context,
  705. predicate: NSPredicate.predicateFor30MinAgo,
  706. key: "date",
  707. ascending: false
  708. )
  709. return await context.perform {
  710. return results.map(\.objectID)
  711. }
  712. }
  713. @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
  714. batteryFromPersistence = objects
  715. }
  716. }
  717. extension Home.StateModel {
  718. // Setup Overrides
  719. private func setupOverrides() {
  720. Task {
  721. let ids = await self.fetchOverrides()
  722. let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  723. await updateOverrideArray(with: overrideObjects)
  724. }
  725. }
  726. private func fetchOverrides() async -> [NSManagedObjectID] {
  727. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  728. ofType: OverrideStored.self,
  729. onContext: context,
  730. predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
  731. key: "date",
  732. ascending: false
  733. )
  734. return await context.perform {
  735. return results.map(\.objectID)
  736. }
  737. }
  738. @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
  739. overrides = objects
  740. }
  741. @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
  742. guard let overrideDuration = override.duration as? Double, overrideDuration != 0 else {
  743. return TimeInterval(60 * 60 * 24) // one day
  744. }
  745. return TimeInterval(overrideDuration * 60) // return seconds
  746. }
  747. @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
  748. guard let overrideTarget = override.target, overrideTarget != 0 else {
  749. return 100 // default
  750. }
  751. return overrideTarget.decimalValue
  752. }
  753. // Setup expired Overrides
  754. private func setupOverrideRunStored() {
  755. Task {
  756. let ids = await self.fetchOverrideRunStored()
  757. let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
  758. .getNSManagedObject(with: ids, context: viewContext)
  759. await updateOverrideRunStoredArray(with: overrideRunObjects)
  760. }
  761. }
  762. private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
  763. let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
  764. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  765. ofType: OverrideRunStored.self,
  766. onContext: context,
  767. predicate: predicate,
  768. key: "startDate",
  769. ascending: false
  770. )
  771. return await context.perform {
  772. return results.map(\.objectID)
  773. }
  774. }
  775. @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
  776. overrideRunStored = objects
  777. }
  778. @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
  779. await viewContext.perform {
  780. do {
  781. guard let object = try self.viewContext.existingObject(with: id) as? OverrideStored else { return }
  782. let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
  783. newOverrideRunStored.id = UUID()
  784. newOverrideRunStored.name = object.name
  785. newOverrideRunStored.startDate = object.date ?? .distantPast
  786. newOverrideRunStored.endDate = Date()
  787. newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
  788. newOverrideRunStored.override = object
  789. newOverrideRunStored.isUploadedToNS = false
  790. } catch {
  791. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
  792. }
  793. }
  794. }
  795. }
  796. extension Home.StateModel {
  797. // Asynchronously preprocess forecast data in a background thread
  798. func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
  799. await Task.detached { [self] () -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] in
  800. // Get the first determination ID from persistence
  801. guard let id = determinationsFromPersistence.first?.objectID else {
  802. return []
  803. }
  804. // Get the forecast IDs for the determination ID
  805. let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
  806. var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
  807. // Use a task group to fetch forecast value IDs concurrently
  808. await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
  809. for forecastID in forecastIDs {
  810. group.addTask {
  811. let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
  812. for: forecastID,
  813. in: self.context
  814. )
  815. return (UUID(), forecastID, forecastValueIDs)
  816. }
  817. }
  818. // Collect the results from the task group
  819. for await (uuid, forecastID, forecastValueIDs) in group {
  820. result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
  821. }
  822. }
  823. return result
  824. }.value
  825. }
  826. // Fetch forecast values for a given data set
  827. func fetchForecastValues(
  828. for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
  829. in context: NSManagedObjectContext
  830. ) async -> (UUID, Forecast?, [ForecastValue]) {
  831. var forecast: Forecast?
  832. var forecastValues: [ForecastValue] = []
  833. do {
  834. try await context.perform {
  835. // Fetch the forecast object
  836. forecast = try context.existingObject(with: data.forecastID) as? Forecast
  837. // Fetch the first 3h of forecast values
  838. for forecastValueID in data.forecastValueIDs.prefix(36) {
  839. if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
  840. forecastValues.append(forecastValue)
  841. }
  842. }
  843. }
  844. } catch {
  845. debugPrint(
  846. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
  847. )
  848. }
  849. return (data.id, forecast, forecastValues)
  850. }
  851. // Update forecast data and UI on the main thread
  852. @MainActor func updateForecastData() async {
  853. // Preprocess forecast data on a background thread
  854. let forecastData = await preprocessForecastData()
  855. var allForecastValues = [[ForecastValue]]()
  856. var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
  857. // Use a task group to fetch forecast values concurrently
  858. await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
  859. for data in forecastData {
  860. group.addTask {
  861. await self.fetchForecastValues(for: data, in: self.viewContext)
  862. }
  863. }
  864. // Collect the results from the task group
  865. for await (id, forecast, forecastValues) in group {
  866. guard let forecast = forecast, !forecastValues.isEmpty else { continue }
  867. allForecastValues.append(forecastValues)
  868. preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
  869. }
  870. }
  871. self.preprocessedData = preprocessedData
  872. // Ensure there are forecast values to process
  873. guard !allForecastValues.isEmpty else {
  874. minForecast = []
  875. maxForecast = []
  876. return
  877. }
  878. minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
  879. guard minCount > 0 else { return }
  880. // Copy allForecastValues to a local constant for thread safety
  881. let localAllForecastValues = allForecastValues
  882. // Calculate min and max forecast values in a background task
  883. let (minResult, maxResult) = await Task.detached {
  884. let minForecast = (0 ..< self.minCount).map { index in
  885. localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.min() ?? 0
  886. }
  887. let maxForecast = (0 ..< self.minCount).map { index in
  888. localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.max() ?? 0
  889. }
  890. return (minForecast, maxForecast)
  891. }.value
  892. // Update the properties on the main thread
  893. minForecast = minResult
  894. maxForecast = maxResult
  895. }
  896. }