StatStateModel.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import CoreData
  2. import Foundation
  3. import Observation
  4. import SwiftUI
  5. import Swinject
  6. extension Stat {
  7. @Observable final class StateModel: BaseStateModel<Provider> {
  8. @ObservationIgnored @Injected() var settings: SettingsManager!
  9. var highLimit: Decimal = 180
  10. var lowLimit: Decimal = 70
  11. var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
  12. var units: GlucoseUnits = .mgdL
  13. var useFPUconversion: Bool = false
  14. var glucoseFromPersistence: [GlucoseStored] = []
  15. var loopStatRecords: [LoopStatRecord] = []
  16. var loopStats: [LoopStatsProcessedData] = []
  17. var groupedLoopStats: [LoopStatsByPeriod] = []
  18. var bolusStats: [BolusStats] = []
  19. var hourlyStats: [HourlyStats] = []
  20. var glucoseRangeStats: [GlucoseRangeStats] = []
  21. // Cache for Meal Stats
  22. var hourlyMealStats: [MealStats] = []
  23. var dailyMealStats: [MealStats] = []
  24. var dailyAveragesCache: [Date: (carbs: Double, fat: Double, protein: Double)] = [:]
  25. // Cache for TDD Stats
  26. var hourlyTDDStats: [TDDStats] = []
  27. var dailyTDDStats: [TDDStats] = []
  28. var tddAveragesCache: [Date: Double] = [:]
  29. // Cache for Bolus Stats
  30. var hourlyBolusStats: [BolusStats] = []
  31. var dailyBolusStats: [BolusStats] = []
  32. var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
  33. var bolusTotalsCache: [(Date, total: Double)] = []
  34. // Selected Duration for Glucose Stats
  35. var selectedIntervalForGlucoseStats: StatsTimeIntervalWithToday = .today {
  36. didSet {
  37. setupGlucoseArray(for: selectedIntervalForGlucoseStats)
  38. }
  39. }
  40. // Selected Duration for Insulin Stats
  41. var selectedIntervalForInsulinStats: StatsTimeInterval = .day
  42. // Selected Duration for Meal Stats
  43. var selectedIntervalForMealStats: StatsTimeInterval = .day
  44. // Selected Duration for Loop Stats
  45. var selectedIntervalForLoopStats: StatsTimeIntervalWithToday = .today {
  46. didSet {
  47. setupLoopStatRecords()
  48. }
  49. }
  50. // Selected Glucose Chart Type
  51. var selectedGlucoseChartType: GlucoseChartType = .percentile
  52. // Selected Insulin Chart Type
  53. var selectedInsulinChartType: InsulinChartType = .totalDailyDose
  54. // Selected Looping Chart Type
  55. var selectedLoopingChartType: LoopingChartType = .loopingPerformance
  56. // Selected Meal Chart Type
  57. var selectedMealChartType: MealChartType = .totalMeals
  58. // Fetching Contexts
  59. let context = CoreDataStack.shared.newTaskContext()
  60. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  61. let tddTaskContext = CoreDataStack.shared.newTaskContext()
  62. let loopTaskContext = CoreDataStack.shared.newTaskContext()
  63. let mealTaskContext = CoreDataStack.shared.newTaskContext()
  64. let bolusTaskContext = CoreDataStack.shared.newTaskContext()
  65. override func subscribe() {
  66. setupGlucoseArray(for: .today)
  67. setupTDDStats()
  68. setupBolusStats()
  69. setupLoopStatRecords()
  70. setupMealStats()
  71. units = settingsManager.settings.units
  72. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  73. useFPUconversion = settingsManager.settings.useFPUconversion
  74. }
  75. func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {
  76. Task {
  77. let ids = await fetchGlucose(for: interval)
  78. await updateGlucoseArray(with: ids)
  79. // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
  80. async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
  81. async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
  82. _ = await (hourlyStats, glucoseRangeStats)
  83. }
  84. }
  85. private func fetchGlucose(for interval: StatsTimeIntervalWithToday) async -> [NSManagedObjectID] {
  86. do {
  87. let predicate: NSPredicate
  88. switch interval {
  89. case .day:
  90. predicate = NSPredicate.glucoseForStatsDay
  91. case .week:
  92. predicate = NSPredicate.glucoseForStatsWeek
  93. case .today:
  94. predicate = NSPredicate.glucoseForStatsToday
  95. case .month:
  96. predicate = NSPredicate.glucoseForStatsMonth
  97. case .total:
  98. predicate = NSPredicate.glucoseForStatsTotal
  99. }
  100. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  101. ofType: GlucoseStored.self,
  102. onContext: context,
  103. predicate: predicate,
  104. key: "date",
  105. ascending: false,
  106. batchSize: 100,
  107. propertiesToFetch: ["glucose", "objectID"]
  108. )
  109. return try await context.perform {
  110. guard let fetchedResults = results as? [[String: Any]] else {
  111. throw CoreDataError.fetchError(function: #function, file: #file)
  112. }
  113. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  114. }
  115. } catch {
  116. debug(.default, "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)")
  117. return []
  118. }
  119. }
  120. @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
  121. do {
  122. let glucoseObjects = try IDs.compactMap { id in
  123. try viewContext.existingObject(with: id) as? GlucoseStored
  124. }
  125. glucoseFromPersistence = glucoseObjects
  126. } catch {
  127. debugPrint(
  128. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
  129. )
  130. }
  131. }
  132. }
  133. @Observable final class UpdateTimer {
  134. private var workItem: DispatchWorkItem?
  135. /// Schedules a delayed update action
  136. /// - Parameter action: The closure to execute after the delay
  137. /// Cancels any previously scheduled update before scheduling a new one
  138. func scheduleUpdate(action: @escaping () -> Void) {
  139. workItem?.cancel()
  140. let newWorkItem = DispatchWorkItem {
  141. action()
  142. }
  143. workItem = newWorkItem
  144. DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: newWorkItem)
  145. }
  146. }
  147. }
  148. // MARK: Stats Types + Enums
  149. extension Stat.StateModel {
  150. /// Defines the available types of glucose charts
  151. enum GlucoseChartType: String, CaseIterable {
  152. /// Ambulatory Glucose Profile showing percentile ranges
  153. case percentile = "Percentile"
  154. /// Time-based distribution of glucose ranges
  155. case distribution = "Distribution"
  156. var displayName: String {
  157. switch self {
  158. case .percentile:
  159. return String(localized: "Percentile")
  160. case .distribution:
  161. return String(localized: "Distribution")
  162. }
  163. }
  164. }
  165. /// Defines the available types of insulin charts
  166. enum InsulinChartType: String, CaseIterable {
  167. /// Shows total daily insulin doses
  168. case totalDailyDose = "Total Daily Dose"
  169. /// Shows distribution of bolus types
  170. case bolusDistribution = "Bolus Distribution"
  171. var displayName: String {
  172. switch self {
  173. case .totalDailyDose:
  174. return String(localized: "Total Daily Dose")
  175. case .bolusDistribution:
  176. return String(localized: "Bolus Distribution")
  177. }
  178. }
  179. }
  180. /// Defines the available types of looping charts
  181. enum LoopingChartType: String, CaseIterable {
  182. /// Shows loop completion and success rates
  183. case loopingPerformance = "Looping Performance"
  184. /// Shows CGM connection status over time
  185. case cgmConnectionTrace = "CGM Connection Trace"
  186. /// Shows Trio pump uptime statistics
  187. case trioUpTime = "Trio Up-Time"
  188. var displayName: String {
  189. switch self {
  190. case .loopingPerformance:
  191. return String(localized: "Looping Performance")
  192. case .cgmConnectionTrace:
  193. return String(localized: "CGM Connection Trace")
  194. case .trioUpTime:
  195. return String(localized: "Trio Up-Time")
  196. }
  197. }
  198. }
  199. /// Defines the available types of meal charts
  200. enum MealChartType: String, CaseIterable {
  201. /// Shows total meal statistics
  202. case totalMeals = "Total Meals"
  203. /// Shows correlation between meals and glucose excursions
  204. case mealToHypoHyperDistribution = "Meal to Hypo/Hyper"
  205. var displayName: String {
  206. switch self {
  207. case .totalMeals:
  208. return String(localized: "Total Meals")
  209. case .mealToHypoHyperDistribution:
  210. return String(localized: "Meal to Hypo/Hyper")
  211. }
  212. }
  213. }
  214. /// Defines the available time periods for duration-based statistics including 'Today' (time since midnight until now)
  215. enum StatsTimeIntervalWithToday: String, CaseIterable, Identifiable {
  216. /// Current day
  217. case today
  218. /// Single day view
  219. case day = "D"
  220. /// Week view
  221. case week = "W"
  222. /// Month view
  223. case month = "M"
  224. /// Three month view
  225. case total = "3 M"
  226. var id: Self { self }
  227. var displayName: String {
  228. switch self {
  229. case .today:
  230. return String(localized: "Today")
  231. case .day:
  232. return String(localized: "D", comment: "Abbreviation for day")
  233. case .week:
  234. return String(localized: "W", comment: "Abbreviation for week")
  235. case .month:
  236. return String(localized: "M", comment: "Abbreviation for month")
  237. case .total:
  238. return String(localized: "3 M", comment: "Abbreviation for three months")
  239. }
  240. }
  241. }
  242. /// Defines the available time periods for duration-based statistics
  243. enum StatsTimeInterval: String, CaseIterable, Identifiable {
  244. /// Single day interval
  245. case day = "D"
  246. /// Week interval
  247. case week = "W"
  248. /// Month interval
  249. case month = "M"
  250. /// Three month interval
  251. case total = "3 M"
  252. var id: Self { self }
  253. var displayName: String {
  254. switch self {
  255. case .day:
  256. return String(localized: "D", comment: "Abbreviation for day")
  257. case .week:
  258. return String(localized: "W", comment: "Abbreviation for week")
  259. case .month:
  260. return String(localized: "M", comment: "Abbreviation for month")
  261. case .total:
  262. return String(localized: "3 M", comment: "Abbreviation for three months")
  263. }
  264. }
  265. }
  266. /// Defines the main categories of statistics available in the app
  267. enum StatisticViewType: String, CaseIterable, Identifiable {
  268. /// Glucose-related statistics including AGP and distributions
  269. case glucose
  270. /// Insulin delivery statistics including TDD and bolus distributions
  271. case insulin
  272. /// Loop performance and system status statistics
  273. case looping
  274. /// Meal-related statistics and correlations
  275. case meals
  276. var id: String { rawValue }
  277. var displayName: String {
  278. switch self {
  279. case .glucose:
  280. return String(localized: "Glucose", comment: "Title for glucose-related statistics")
  281. case .insulin:
  282. return String(localized: "Insulin", comment: "Title for insulin-related statistics")
  283. case .looping:
  284. return String(localized: "Looping", comment: "Title for looping and system statistics")
  285. case .meals:
  286. return String(localized: "Meals", comment: "Title for meal-related statistics")
  287. }
  288. }
  289. }
  290. }