HomeStateModel.swift 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132
  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 smooth = 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 isTempTargetActive: Bool = false
  62. @Published var roundedTotalBolus: String = ""
  63. @Published var selectedTab: Int = 0
  64. @Published var waitForSuggestion: Bool = false
  65. @Published var glucoseFromPersistence: [GlucoseStored] = []
  66. @Published var manualGlucoseFromPersistence: [GlucoseStored] = []
  67. @Published var carbsFromPersistence: [CarbEntryStored] = []
  68. @Published var fpusFromPersistence: [CarbEntryStored] = []
  69. @Published var determinationsFromPersistence: [OrefDetermination] = []
  70. @Published var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
  71. @Published var insulinFromPersistence: [PumpEventStored] = []
  72. @Published var tempBasals: [PumpEventStored] = []
  73. @Published var suspensions: [PumpEventStored] = []
  74. @Published var batteryFromPersistence: [OpenAPS_Battery] = []
  75. @Published var lastPumpBolus: PumpEventStored?
  76. @Published var overrides: [OverrideStored] = []
  77. @Published var overrideRunStored: [OverrideRunStored] = []
  78. @Published var isOverrideCancelled: Bool = false
  79. @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
  80. @Published var pumpStatusHighlightMessage: String? = nil
  81. @Published var cgmAvailable: Bool = false
  82. @Published var showCarbsRequiredBadge: Bool = true
  83. private(set) var setupPumpType: PumpConfig.PumpType = .minimed
  84. @Published var minForecast: [Int] = []
  85. @Published var maxForecast: [Int] = []
  86. @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
  87. @Published var forecastDisplayType: ForecastDisplayType = .cone
  88. @Published var gradientStops: [Gradient.Stop] = []
  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. smooth = 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. private func calculateGradientStops() async {
  306. let glucoseValues = glucoseFromPersistence
  307. .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
  308. let calculatedStops = await GradientStops.calculateGradientStops(
  309. lowGlucose: lowGlucose,
  310. highGlucose: highGlucose,
  311. glucoseValues: glucoseValues
  312. )
  313. await MainActor.run {
  314. self.gradientStops = calculatedStops
  315. }
  316. }
  317. func runLoop() {
  318. provider.heartbeatNow()
  319. }
  320. func showProgressView() {
  321. glucoseStorage
  322. .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
  323. }
  324. func cancelBolus() {
  325. Task {
  326. await apsManager.cancelBolus()
  327. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  328. await apsManager.determineBasalSync()
  329. }
  330. }
  331. @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
  332. do {
  333. let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
  334. profileToCancel?.enabled = false
  335. await saveToOverrideRunStored(withID: id)
  336. guard viewContext.hasChanges else { return }
  337. try viewContext.save()
  338. Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
  339. } catch {
  340. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
  341. }
  342. }
  343. func calculateTINS() -> String {
  344. let startTime = calculateStartTime(hours: Int(hours))
  345. let totalBolus = calculateTotalBolus(from: insulinFromPersistence, since: startTime)
  346. let totalBasal = calculateTotalBasal(from: insulinFromPersistence, since: startTime)
  347. let totalInsulin = totalBolus + totalBasal
  348. return formatInsulinAmount(totalInsulin)
  349. }
  350. private func calculateStartTime(hours: Int) -> Date {
  351. let date = Date()
  352. let calendar = Calendar.current
  353. var offsetComponents = DateComponents()
  354. offsetComponents.hour = -hours
  355. return calendar.date(byAdding: offsetComponents, to: date)!
  356. }
  357. private func calculateTotalBolus(from events: [PumpEventStored], since startTime: Date) -> Double {
  358. let bolusEvents = events.filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.bolus.rawValue }
  359. return bolusEvents.compactMap { $0.bolus?.amount?.doubleValue }.reduce(0, +)
  360. }
  361. private func calculateTotalBasal(from events: [PumpEventStored], since startTime: Date) -> Double {
  362. let basalEvents = events
  363. .filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.tempBasal.rawValue }
  364. .sorted { $0.timestamp ?? .distantPast < $1.timestamp ?? .distantPast }
  365. var basalDurations: [Double] = []
  366. for (index, basalEntry) in basalEvents.enumerated() {
  367. if index + 1 < basalEvents.count {
  368. let nextEntry = basalEvents[index + 1]
  369. let durationInSeconds = nextEntry.timestamp?.timeIntervalSince(basalEntry.timestamp ?? Date()) ?? 0
  370. basalDurations.append(durationInSeconds / 3600) // Conversion to hours
  371. }
  372. }
  373. return zip(basalEvents, basalDurations).map { entry, duration in
  374. guard let rate = entry.tempBasal?.rate?.doubleValue else { return 0 }
  375. return rate * duration
  376. }.reduce(0, +)
  377. }
  378. private func formatInsulinAmount(_ amount: Double) -> String {
  379. let roundedAmount = Decimal(round(100 * amount) / 100)
  380. return roundedAmount.formatted()
  381. }
  382. private func setupPumpSettings() {
  383. DispatchQueue.main.async { [weak self] in
  384. guard let self = self else { return }
  385. self.maxBasal = self.provider.pumpSettings().maxBasal
  386. }
  387. }
  388. private func setupBasalProfile() {
  389. DispatchQueue.main.async { [weak self] in
  390. guard let self = self else { return }
  391. self.autotunedBasalProfile = self.provider.autotunedBasalProfile()
  392. self.basalProfile = self.provider.basalProfile()
  393. }
  394. }
  395. private func setupTempTargets() {
  396. DispatchQueue.main.async { [weak self] in
  397. guard let self = self else { return }
  398. self.manualTempBasal = self.apsManager.isManualTempBasal
  399. self.tempTargets = self.provider.tempTargets(hours: self.filteredHours)
  400. }
  401. }
  402. private func setupAnnouncements() {
  403. DispatchQueue.main.async { [weak self] in
  404. guard let self = self else { return }
  405. self.announcement = self.provider.announcement(self.filteredHours)
  406. }
  407. }
  408. private func setupReservoir() {
  409. DispatchQueue.main.async { [weak self] in
  410. guard let self = self else { return }
  411. self.reservoir = self.provider.pumpReservoir()
  412. }
  413. }
  414. private func setupCurrentTempTarget() {
  415. tempTarget = provider.tempTarget()
  416. }
  417. private func setupCurrentPumpTimezone() {
  418. DispatchQueue.main.async { [weak self] in
  419. guard let self = self else { return }
  420. self.timeZone = self.provider.pumpTimeZone()
  421. }
  422. }
  423. func openCGM() {
  424. router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
  425. }
  426. func infoPanelTTPercentage(_ hbt_: Double, _ target: Decimal) -> Decimal {
  427. guard hbt_ != 0 || target != 0 else {
  428. return 0
  429. }
  430. let c = Decimal(hbt_ - 100)
  431. let ratio = min(c / (target + c - 100), maxValue)
  432. return (ratio * 100)
  433. }
  434. }
  435. }
  436. extension Home.StateModel:
  437. GlucoseObserver,
  438. DeterminationObserver,
  439. SettingsObserver,
  440. PumpSettingsObserver,
  441. BasalProfileObserver,
  442. TempTargetsObserver,
  443. PumpReservoirObserver,
  444. PumpTimeZoneObserver,
  445. PumpDeactivatedObserver
  446. {
  447. // TODO: still needed?
  448. func glucoseDidUpdate(_: [BloodGlucose]) {
  449. // setupGlucose()
  450. }
  451. func determinationDidUpdate(_: Determination) {
  452. waitForSuggestion = false
  453. }
  454. func settingsDidChange(_ settings: FreeAPSSettings) {
  455. allowManualTemp = !settings.closedLoop
  456. closedLoop = settingsManager.settings.closedLoop
  457. units = settingsManager.settings.units
  458. manualTempBasal = apsManager.isManualTempBasal
  459. smooth = settingsManager.settings.smoothGlucose
  460. lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
  461. highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
  462. overrideUnit = settingsManager.settings.overrideHbA1cUnit
  463. displayXgridLines = settingsManager.settings.xGridLines
  464. displayYgridLines = settingsManager.settings.yGridLines
  465. thresholdLines = settingsManager.settings.rulerMarks
  466. totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
  467. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  468. forecastDisplayType = settingsManager.settings.forecastDisplayType
  469. cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
  470. displayPumpStatusHighlightMessage()
  471. setupBatteryArray()
  472. }
  473. // TODO: is this ever really triggered? react to MOC changes?
  474. func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
  475. displayPumpStatusHighlightMessage()
  476. }
  477. func pumpSettingsDidChange(_: PumpSettings) {
  478. setupPumpSettings()
  479. setupBatteryArray()
  480. }
  481. func basalProfileDidChange(_: [BasalProfileEntry]) {
  482. setupBasalProfile()
  483. }
  484. func tempTargetsDidUpdate(_: [TempTarget]) {
  485. setupTempTargets()
  486. }
  487. func pumpReservoirDidChange(_: Decimal) {
  488. setupReservoir()
  489. displayPumpStatusHighlightMessage()
  490. }
  491. func pumpDeactivatedDidChange() {
  492. displayPumpStatusHighlightMessage(true)
  493. batteryFromPersistence = []
  494. }
  495. func pumpTimeZoneDidChange(_: TimeZone) {
  496. setupCurrentPumpTimezone()
  497. }
  498. }
  499. extension Home.StateModel: CompletionDelegate {
  500. func completionNotifyingDidComplete(_: CompletionNotifying) {
  501. setupPump = false
  502. }
  503. }
  504. extension Home.StateModel: PumpManagerOnboardingDelegate {
  505. func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
  506. provider.apsManager.pumpManager = pumpManager
  507. if let insulinType = pumpManager.status.insulinType {
  508. settingsManager.updateInsulinCurve(insulinType)
  509. }
  510. }
  511. func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
  512. // nothing to do
  513. }
  514. func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
  515. // TODO:
  516. }
  517. }
  518. // MARK: - Setup Core Data observation
  519. extension Home.StateModel {
  520. /// listens for the notifications sent when the managedObjectContext has saved!
  521. func setupNotification() {
  522. /// custom notification that is sent when a batch insert of glucose objects is done
  523. Foundation.NotificationCenter.default.addObserver(
  524. self,
  525. selector: #selector(handleBatchInsert),
  526. name: .didPerformBatchInsert,
  527. object: nil
  528. )
  529. /// custom notification that is sent when a batch delete of fpus is done
  530. Foundation.NotificationCenter.default.addObserver(
  531. self,
  532. selector: #selector(handleBatchDelete),
  533. name: .didPerformBatchDelete,
  534. object: nil
  535. )
  536. }
  537. @objc private func handleBatchInsert() {
  538. setupFPUsArray()
  539. setupGlucoseArray()
  540. }
  541. @objc private func handleBatchDelete() {
  542. setupFPUsArray()
  543. }
  544. }
  545. // MARK: - Handle Core Data changes and update Arrays to display them in the UI
  546. extension Home.StateModel {
  547. // Setup Glucose
  548. private func setupGlucoseArray() {
  549. Task {
  550. let ids = await self.fetchGlucose()
  551. let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  552. await updateGlucoseArray(with: glucoseObjects)
  553. if smooth {
  554. await calculateGradientStops()
  555. }
  556. }
  557. }
  558. private func fetchGlucose() async -> [NSManagedObjectID] {
  559. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  560. ofType: GlucoseStored.self,
  561. onContext: context,
  562. predicate: NSPredicate.glucose,
  563. key: "date",
  564. ascending: false,
  565. fetchLimit: 288
  566. )
  567. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  568. return await context.perform {
  569. return fetchedResults.map(\.objectID)
  570. }
  571. }
  572. @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
  573. glucoseFromPersistence = objects
  574. }
  575. // Setup Manual Glucose
  576. private func setupManualGlucoseArray() {
  577. Task {
  578. let ids = await self.fetchManualGlucose()
  579. let manualGlucoseObjects: [GlucoseStored] = await CoreDataStack.shared
  580. .getNSManagedObject(with: ids, context: viewContext)
  581. await updateManualGlucoseArray(with: manualGlucoseObjects)
  582. if smooth {
  583. await calculateGradientStops()
  584. }
  585. }
  586. }
  587. private func fetchManualGlucose() async -> [NSManagedObjectID] {
  588. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  589. ofType: GlucoseStored.self,
  590. onContext: context,
  591. predicate: NSPredicate.manualGlucose,
  592. key: "date",
  593. ascending: false,
  594. fetchLimit: 288
  595. )
  596. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  597. return await context.perform {
  598. return fetchedResults.map(\.objectID)
  599. }
  600. }
  601. @MainActor private func updateManualGlucoseArray(with objects: [GlucoseStored]) {
  602. manualGlucoseFromPersistence = objects
  603. }
  604. // Setup Carbs
  605. private func setupCarbsArray() {
  606. Task {
  607. let ids = await self.fetchCarbs()
  608. let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  609. await updateCarbsArray(with: carbObjects)
  610. }
  611. }
  612. private func fetchCarbs() async -> [NSManagedObjectID] {
  613. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  614. ofType: CarbEntryStored.self,
  615. onContext: context,
  616. predicate: NSPredicate.carbsForChart,
  617. key: "date",
  618. ascending: false
  619. )
  620. guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
  621. return await context.perform {
  622. return fetchedResults.map(\.objectID)
  623. }
  624. }
  625. @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
  626. carbsFromPersistence = objects
  627. }
  628. // Setup FPUs
  629. private func setupFPUsArray() {
  630. Task {
  631. let ids = await self.fetchFPUs()
  632. let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  633. await updateFPUsArray(with: fpuObjects)
  634. }
  635. }
  636. private func fetchFPUs() async -> [NSManagedObjectID] {
  637. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  638. ofType: CarbEntryStored.self,
  639. onContext: context,
  640. predicate: NSPredicate.fpusForChart,
  641. key: "date",
  642. ascending: false
  643. )
  644. guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
  645. return await context.perform {
  646. return fetchedResults.map(\.objectID)
  647. }
  648. }
  649. @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
  650. fpusFromPersistence = objects
  651. }
  652. // Custom fetch to more efficiently filter only for cob and iob
  653. private func fetchCobAndIob() async -> [NSManagedObjectID] {
  654. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  655. ofType: OrefDetermination.self,
  656. onContext: context,
  657. predicate: NSPredicate.determinationsForCobIobCharts,
  658. key: "deliverAt",
  659. ascending: false,
  660. batchSize: 50,
  661. propertiesToFetch: ["cob", "iob", "objectID"]
  662. )
  663. guard let fetchedResults = results as? [[String: Any]] else {
  664. return []
  665. }
  666. return await context.perform {
  667. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  668. }
  669. }
  670. // Setup Determinations
  671. private func setupDeterminationsArray() {
  672. Task {
  673. // Get the NSManagedObjectIDs
  674. async let enactedObjectIDs = determinationStorage
  675. .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
  676. async let enactedAndNonEnactedObjectIDs = fetchCobAndIob()
  677. let enactedIDs = await enactedObjectIDs
  678. let enactedAndNonEnactedIDs = await enactedAndNonEnactedObjectIDs
  679. // Get the NSManagedObjects and return them on the Main Thread
  680. await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
  681. await updateDeterminationsArray(with: enactedAndNonEnactedIDs, keyPath: \.enactedAndNonEnactedDeterminations)
  682. await updateForecastData()
  683. }
  684. }
  685. @MainActor private func updateDeterminationsArray(
  686. with IDs: [NSManagedObjectID],
  687. keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
  688. ) async {
  689. // Fetch the objects off the main thread
  690. let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
  691. .getNSManagedObject(with: IDs, context: viewContext)
  692. // Update the array on the main thread
  693. self[keyPath: keyPath] = determinationObjects
  694. }
  695. // Setup Insulin
  696. private func setupInsulinArray() {
  697. Task {
  698. let ids = await self.fetchInsulin()
  699. let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  700. await updateInsulinArray(with: insulinObjects)
  701. }
  702. }
  703. private func fetchInsulin() async -> [NSManagedObjectID] {
  704. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  705. ofType: PumpEventStored.self,
  706. onContext: context,
  707. predicate: NSPredicate.pumpHistoryLast24h,
  708. key: "timestamp",
  709. ascending: true
  710. )
  711. guard let pumpEvents = results as? [PumpEventStored] else {
  712. return []
  713. }
  714. return await context.perform {
  715. return pumpEvents.map(\.objectID)
  716. }
  717. }
  718. @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
  719. insulinFromPersistence = insulinObjects
  720. // Filter tempbasals
  721. manualTempBasal = apsManager.isManualTempBasal
  722. tempBasals = insulinFromPersistence.filter({ $0.tempBasal != nil })
  723. // Suspension and resume events
  724. suspensions = insulinFromPersistence.filter {
  725. $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
  726. }
  727. let lastSuspension = suspensions.last
  728. pumpSuspended = tempBasals.last?.timestamp ?? Date() > lastSuspension?.timestamp ?? .distantPast && lastSuspension?
  729. .type == EventType.pumpSuspend.rawValue
  730. }
  731. // Setup Last Bolus to display the bolus progress bar
  732. // 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
  733. private func setupLastBolus() {
  734. Task {
  735. guard let id = await self.fetchLastBolus() else { return }
  736. await updateLastBolus(with: id)
  737. }
  738. }
  739. private func fetchLastBolus() async -> NSManagedObjectID? {
  740. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  741. ofType: PumpEventStored.self,
  742. onContext: context,
  743. predicate: NSPredicate.lastPumpBolus,
  744. key: "timestamp",
  745. ascending: false,
  746. fetchLimit: 1
  747. )
  748. guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
  749. return await context.perform {
  750. return fetchedResults.map(\.objectID).first
  751. }
  752. }
  753. @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
  754. do {
  755. lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
  756. } catch {
  757. debugPrint(
  758. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
  759. )
  760. }
  761. }
  762. // Setup Battery
  763. private func setupBatteryArray() {
  764. Task {
  765. let ids = await self.fetchBattery()
  766. let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  767. await updateBatteryArray(with: batteryObjects)
  768. }
  769. }
  770. private func fetchBattery() async -> [NSManagedObjectID] {
  771. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  772. ofType: OpenAPS_Battery.self,
  773. onContext: context,
  774. predicate: NSPredicate.predicateFor30MinAgo,
  775. key: "date",
  776. ascending: false
  777. )
  778. guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
  779. return await context.perform {
  780. return fetchedResults.map(\.objectID)
  781. }
  782. }
  783. @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
  784. batteryFromPersistence = objects
  785. }
  786. }
  787. extension Home.StateModel {
  788. // Setup Overrides
  789. private func setupOverrides() {
  790. Task {
  791. let ids = await self.fetchOverrides()
  792. let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
  793. await updateOverrideArray(with: overrideObjects)
  794. }
  795. }
  796. private func fetchOverrides() async -> [NSManagedObjectID] {
  797. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  798. ofType: OverrideStored.self,
  799. onContext: context,
  800. predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
  801. key: "date",
  802. ascending: false
  803. )
  804. guard let fetchedResults = results as? [OverrideStored] else { return [] }
  805. return await context.perform {
  806. return fetchedResults.map(\.objectID)
  807. }
  808. }
  809. @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
  810. overrides = objects
  811. }
  812. @MainActor func calculateDuration(override: OverrideStored) -> TimeInterval {
  813. guard let overrideDuration = override.duration as? Double, overrideDuration != 0 else {
  814. return TimeInterval(60 * 60 * 24) // one day
  815. }
  816. return TimeInterval(overrideDuration * 60) // return seconds
  817. }
  818. @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
  819. guard let overrideTarget = override.target, overrideTarget != 0 else {
  820. return 100 // default
  821. }
  822. return overrideTarget.decimalValue
  823. }
  824. // Setup expired Overrides
  825. private func setupOverrideRunStored() {
  826. Task {
  827. let ids = await self.fetchOverrideRunStored()
  828. let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
  829. .getNSManagedObject(with: ids, context: viewContext)
  830. await updateOverrideRunStoredArray(with: overrideRunObjects)
  831. }
  832. }
  833. private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
  834. let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
  835. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  836. ofType: OverrideRunStored.self,
  837. onContext: context,
  838. predicate: predicate,
  839. key: "startDate",
  840. ascending: false
  841. )
  842. guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
  843. return await context.perform {
  844. return fetchedResults.map(\.objectID)
  845. }
  846. }
  847. @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
  848. overrideRunStored = objects
  849. }
  850. @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
  851. await viewContext.perform {
  852. do {
  853. guard let object = try self.viewContext.existingObject(with: id) as? OverrideStored else { return }
  854. let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
  855. newOverrideRunStored.id = UUID()
  856. newOverrideRunStored.name = object.name
  857. newOverrideRunStored.startDate = object.date ?? .distantPast
  858. newOverrideRunStored.endDate = Date()
  859. newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
  860. newOverrideRunStored.override = object
  861. newOverrideRunStored.isUploadedToNS = false
  862. } catch {
  863. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
  864. }
  865. }
  866. }
  867. }
  868. extension Home.StateModel {
  869. // Asynchronously preprocess forecast data in a background thread
  870. func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
  871. await Task.detached { [self] () -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] in
  872. // Get the first determination ID from persistence
  873. guard let id = enactedAndNonEnactedDeterminations.first?.objectID else {
  874. return []
  875. }
  876. // Get the forecast IDs for the determination ID
  877. let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
  878. var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
  879. // Use a task group to fetch forecast value IDs concurrently
  880. await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
  881. for forecastID in forecastIDs {
  882. group.addTask {
  883. let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
  884. for: forecastID,
  885. in: self.context
  886. )
  887. return (UUID(), forecastID, forecastValueIDs)
  888. }
  889. }
  890. // Collect the results from the task group
  891. for await (uuid, forecastID, forecastValueIDs) in group {
  892. result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
  893. }
  894. }
  895. return result
  896. }.value
  897. }
  898. // Fetch forecast values for a given data set
  899. func fetchForecastValues(
  900. for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
  901. in context: NSManagedObjectContext
  902. ) async -> (UUID, Forecast?, [ForecastValue]) {
  903. var forecast: Forecast?
  904. var forecastValues: [ForecastValue] = []
  905. do {
  906. try await context.perform {
  907. // Fetch the forecast object
  908. forecast = try context.existingObject(with: data.forecastID) as? Forecast
  909. // Fetch the first 3h of forecast values
  910. for forecastValueID in data.forecastValueIDs.prefix(36) {
  911. if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
  912. forecastValues.append(forecastValue)
  913. }
  914. }
  915. }
  916. } catch {
  917. debugPrint(
  918. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
  919. )
  920. }
  921. return (data.id, forecast, forecastValues)
  922. }
  923. // Update forecast data and UI on the main thread
  924. @MainActor func updateForecastData() async {
  925. // Preprocess forecast data on a background thread
  926. let forecastData = await preprocessForecastData()
  927. var allForecastValues = [[ForecastValue]]()
  928. var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
  929. // Use a task group to fetch forecast values concurrently
  930. await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
  931. for data in forecastData {
  932. group.addTask {
  933. await self.fetchForecastValues(for: data, in: self.viewContext)
  934. }
  935. }
  936. // Collect the results from the task group
  937. for await (id, forecast, forecastValues) in group {
  938. guard let forecast = forecast, !forecastValues.isEmpty else { continue }
  939. allForecastValues.append(forecastValues)
  940. preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
  941. }
  942. }
  943. self.preprocessedData = preprocessedData
  944. // Ensure there are forecast values to process
  945. guard !allForecastValues.isEmpty else {
  946. minForecast = []
  947. maxForecast = []
  948. return
  949. }
  950. minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
  951. guard minCount > 0 else { return }
  952. // Copy allForecastValues to a local constant for thread safety
  953. let localAllForecastValues = allForecastValues
  954. // Calculate min and max forecast values in a background task
  955. let (minResult, maxResult) = await Task.detached {
  956. let minForecast = (0 ..< self.minCount).map { index in
  957. localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.min() ?? 0
  958. }
  959. let maxForecast = (0 ..< self.minCount).map { index in
  960. localAllForecastValues.compactMap { $0.indices.contains(index) ? Int($0[index].value) : nil }.max() ?? 0
  961. }
  962. return (minForecast, maxForecast)
  963. }.value
  964. // Update the properties on the main thread
  965. minForecast = minResult
  966. maxForecast = maxResult
  967. }
  968. }