HomeStateModel.swift 43 KB

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