HomeStateModel.swift 40 KB

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