HomeStateModel.swift 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  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 = 70
  50. @Published var highGlucose: Decimal = 180
  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 = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
  123. highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
  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 = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
  410. highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
  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. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  513. return await context.perform {
  514. return fetchedResults.map(\.objectID)
  515. }
  516. }
  517. @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
  518. glucoseFromPersistence = objects
  519. }
  520. // Setup Manual Glucose
  521. private func setupManualGlucoseArray() {
  522. Task {
  523. let ids = await self.fetchManualGlucose()
  524. let manualGlucoseObjects: [GlucoseStored] = await CoreDataStack.shared
  525. .getNSManagedObject(with: ids, context: viewContext)
  526. await updateManualGlucoseArray(with: manualGlucoseObjects)
  527. }
  528. }
  529. private func fetchManualGlucose() async -> [NSManagedObjectID] {
  530. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  531. ofType: GlucoseStored.self,
  532. onContext: context,
  533. predicate: NSPredicate.manualGlucose,
  534. key: "date",
  535. ascending: false,
  536. fetchLimit: 288
  537. )
  538. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  539. return await context.perform {
  540. return fetchedResults.map(\.objectID)
  541. }
  542. }
  543. @MainActor private func updateManualGlucoseArray(with objects: [GlucoseStored]) {
  544. manualGlucoseFromPersistence = objects
  545. }
  546. // Setup Carbs
  547. private func setupCarbsArray() {
  548. Task {
  549. let ids = await self.fetchCarbs()
  550. let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  551. await updateCarbsArray(with: carbObjects)
  552. }
  553. }
  554. private func fetchCarbs() async -> [NSManagedObjectID] {
  555. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  556. ofType: CarbEntryStored.self,
  557. onContext: context,
  558. predicate: NSPredicate.carbsForChart,
  559. key: "date",
  560. ascending: false
  561. )
  562. guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
  563. return await context.perform {
  564. return fetchedResults.map(\.objectID)
  565. }
  566. }
  567. @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
  568. carbsFromPersistence = objects
  569. }
  570. // Setup FPUs
  571. private func setupFPUsArray() {
  572. Task {
  573. let ids = await self.fetchFPUs()
  574. let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  575. await updateFPUsArray(with: fpuObjects)
  576. }
  577. }
  578. private func fetchFPUs() async -> [NSManagedObjectID] {
  579. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  580. ofType: CarbEntryStored.self,
  581. onContext: context,
  582. predicate: NSPredicate.fpusForChart,
  583. key: "date",
  584. ascending: false
  585. )
  586. guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
  587. return await context.perform {
  588. return fetchedResults.map(\.objectID)
  589. }
  590. }
  591. @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
  592. fpusFromPersistence = objects
  593. }
  594. // Custom fetch to more efficiently filter only for cob and iob
  595. private func fetchCobAndIob() async -> [NSManagedObjectID] {
  596. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  597. ofType: OrefDetermination.self,
  598. onContext: context,
  599. predicate: NSPredicate.determinationsForCobIobCharts,
  600. key: "deliverAt",
  601. ascending: false,
  602. batchSize: 50,
  603. propertiesToFetch: ["cob", "iob", "objectID"]
  604. )
  605. guard let fetchedResults = results as? [[String: Any]] else {
  606. return []
  607. }
  608. return await context.perform {
  609. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  610. }
  611. }
  612. // Setup Determinations
  613. private func setupDeterminationsArray() {
  614. Task {
  615. // Get the NSManagedObjectIDs
  616. async let enactedObjectIDs = determinationStorage
  617. .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
  618. async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
  619. let enactedIDs = await enactedObjectIDs
  620. let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
  621. // Get the NSManagedObjects and return them on the Main Thread
  622. await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
  623. await updateDeterminationsArray(with: enactedAndNonEnactedIDs, keyPath: \.enactedAndNonEnactedDeterminations)
  624. await updateForecastData()
  625. }
  626. }
  627. @MainActor private func updateDeterminationsArray(
  628. with IDs: [NSManagedObjectID],
  629. keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
  630. ) async {
  631. // Fetch the objects off the main thread
  632. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  633. .getNSManagedObject(with: IDs, context: viewContext)
  634. // Update the array on the main thread
  635. self[keyPath: keyPath] = determinationObjects
  636. }
  637. // Setup Insulin
  638. private func setupInsulinArray() {
  639. Task {
  640. let ids = await self.fetchInsulin()
  641. let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  642. await updateInsulinArray(with: insulinObjects)
  643. }
  644. }
  645. private func fetchInsulin() async -> [NSManagedObjectID] {
  646. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  647. ofType: PumpEventStored.self,
  648. onContext: context,
  649. predicate: NSPredicate.pumpHistoryLast24h,
  650. key: "timestamp",
  651. ascending: true
  652. )
  653. guard let pumpEvents = results as? [PumpEventStored] else {
  654. return []
  655. }
  656. return await context.perform {
  657. return pumpEvents.map(\.objectID)
  658. }
  659. }
  660. @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
  661. insulinFromPersistence = insulinObjects
  662. // Filter tempbasals
  663. manualTempBasal = apsManager.isManualTempBasal
  664. tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
  665. // Suspension and resume events
  666. suspensions = insulinFromPersistence.filter {
  667. $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
  668. }
  669. let lastSuspension = suspensions.last
  670. pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
  671. .type == EventType.pumpSuspend.rawValue
  672. }
  673. // Setup Last Bolus to display the bolus progress bar
  674. // 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
  675. private func setupLastBolus() {
  676. Task {
  677. guard let id = await self.fetchLastBolus() else { return }
  678. await updateLastBolus(with: id)
  679. }
  680. }
  681. private func fetchLastBolus() async -> NSManagedObjectID? {
  682. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  683. ofType: PumpEventStored.self,
  684. onContext: context,
  685. predicate: NSPredicate.lastPumpBolus,
  686. key: "timestamp",
  687. ascending: false,
  688. fetchLimit: 1
  689. )
  690. guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
  691. return await context.perform {
  692. return fetchedResults.map(\.objectID).first
  693. }
  694. }
  695. @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
  696. do {
  697. lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
  698. } catch {
  699. debugPrint(
  700. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
  701. )
  702. }
  703. }
  704. // Setup Battery
  705. private func setupBatteryArray() {
  706. Task {
  707. let ids = await self.fetchBattery()
  708. let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  709. await updateBatteryArray(with: batteryObjects)
  710. }
  711. }
  712. private func fetchBattery() async -> [NSManagedObjectID] {
  713. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  714. ofType: OpenAPS_Battery.self,
  715. onContext: context,
  716. predicate: NSPredicate.predicateFor30MinAgo,
  717. key: "date",
  718. ascending: false
  719. )
  720. guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
  721. return await context.perform {
  722. return fetchedResults.map(\.objectID)
  723. }
  724. }
  725. @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
  726. batteryFromPersistence = objects
  727. }
  728. }
  729. extension Home.StateModel {
  730. // Setup Overrides
  731. private func setupOverrides() {
  732. Task {
  733. let ids = await self.fetchOverrides()
  734. let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  735. await updateOverrideArray(with: overrideObjects)
  736. }
  737. }
  738. private func fetchOverrides() async -> [NSManagedObjectID] {
  739. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  740. ofType: OverrideStored.self,
  741. onContext: context,
  742. predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
  743. key: "date",
  744. ascending: false
  745. )
  746. guard let fetchedResults = results as? [OverrideStored] else { return [] }
  747. return await context.perform {
  748. return fetchedResults.map(\.objectID)
  749. }
  750. }
  751. @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
  752. overrides = objects
  753. }
  754. @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
  755. guard let overrideDuration = override.duration as? Double, overrideDuration != 0 else {
  756. return TimeInterval(60 * 60 * 24) // one day
  757. }
  758. return TimeInterval(overrideDuration * 60) // return seconds
  759. }
  760. @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
  761. guard let overrideTarget = override.target, overrideTarget != 0 else {
  762. return 100 // default
  763. }
  764. return overrideTarget.decimalValue
  765. }
  766. // Setup expired Overrides
  767. private func setupOverrideRunStored() {
  768. Task {
  769. let ids = await self.fetchOverrideRunStored()
  770. let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
  771. .getNSManagedObject(with: ids, context: viewContext)
  772. await updateOverrideRunStoredArray(with: overrideRunObjects)
  773. }
  774. }
  775. private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
  776. let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
  777. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  778. ofType: OverrideRunStored.self,
  779. onContext: context,
  780. predicate: predicate,
  781. key: "startDate",
  782. ascending: false
  783. )
  784. guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
  785. return await context.perform {
  786. return fetchedResults.map(\.objectID)
  787. }
  788. }
  789. @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
  790. overrideRunStored = objects
  791. }
  792. @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
  793. await viewContext.perform {
  794. do {
  795. guard let object = try self.viewContext.existingObject(with: id) as? OverrideStored else { return }
  796. let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
  797. newOverrideRunStored.id = UUID()
  798. newOverrideRunStored.name = object.name
  799. newOverrideRunStored.startDate = object.date ?? .distantPast
  800. newOverrideRunStored.endDate = Date()
  801. newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
  802. newOverrideRunStored.override = object
  803. newOverrideRunStored.isUploadedToNS = false
  804. } catch {
  805. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
  806. }
  807. }
  808. }
  809. }
  810. extension Home.StateModel {
  811. // Asynchronously preprocess forecast data in a background thread
  812. func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
  813. await Task.detached { [self] () -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] in
  814. // Get the first determination ID from persistence
  815. guard let id = enactedAndNonEnactedDeterminations.first?.objectID else {
  816. return []
  817. }
  818. // Get the forecast IDs for the determination ID
  819. let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
  820. var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
  821. // Use a task group to fetch forecast value IDs concurrently
  822. await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
  823. for forecastID in forecastIDs {
  824. group.addTask {
  825. let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
  826. for: forecastID,
  827. in: self.context
  828. )
  829. return (UUID(), forecastID, forecastValueIDs)
  830. }
  831. }
  832. // Collect the results from the task group
  833. for await (uuid, forecastID, forecastValueIDs) in group {
  834. result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
  835. }
  836. }
  837. return result
  838. }.value
  839. }
  840. // Fetch forecast values for a given data set
  841. func fetchForecastValues(
  842. for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
  843. in context: NSManagedObjectContext
  844. ) async -> (UUID, Forecast?, [ForecastValue]) {
  845. var forecast: Forecast?
  846. var forecastValues: [ForecastValue] = []
  847. do {
  848. try await context.perform {
  849. // Fetch the forecast object
  850. forecast = try context.existingObject(with: data.forecastID) as? Forecast
  851. // Fetch the first 3h of forecast values
  852. for forecastValueID in data.forecastValueIDs.prefix(36) {
  853. if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
  854. forecastValues.append(forecastValue)
  855. }
  856. }
  857. }
  858. } catch {
  859. debugPrint(
  860. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
  861. )
  862. }
  863. return (data.id, forecast, forecastValues)
  864. }
  865. // Update forecast data and UI on the main thread
  866. @MainActor func updateForecastData() async {
  867. // Preprocess forecast data on a background thread
  868. let forecastData = await preprocessForecastData()
  869. var allForecastValues = [[ForecastValue]]()
  870. var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
  871. // Use a task group to fetch forecast values concurrently
  872. await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
  873. for data in forecastData {
  874. group.addTask {
  875. await self.fetchForecastValues(for: data, in: self.viewContext)
  876. }
  877. }
  878. // Collect the results from the task group
  879. for await (id, forecast, forecastValues) in group {
  880. guard let forecast = forecast, !forecastValues.isEmpty else { continue }
  881. allForecastValues.append(forecastValues)
  882. preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
  883. }
  884. }
  885. self.preprocessedData = preprocessedData
  886. // Ensure there are forecast values to process
  887. guard !allForecastValues.isEmpty else {
  888. minForecast = []
  889. maxForecast = []
  890. return
  891. }
  892. minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
  893. guard minCount > 0 else { return }
  894. // Copy allForecastValues to a local constant for thread safety
  895. let localAllForecastValues = allForecastValues
  896. // Calculate min and max forecast values in a background task
  897. let (minResult, maxResult) = await Task.detached {
  898. let minForecast = (0 ..< self.minCount).map { index in
  899. localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.min() ?? 0
  900. }
  901. let maxForecast = (0 ..< self.minCount).map { index in
  902. localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.max() ?? 0
  903. }
  904. return (minForecast, maxForecast)
  905. }.value
  906. // Update the properties on the main thread
  907. minForecast = minResult
  908. maxForecast = maxResult
  909. }
  910. }