HomeStateModel.swift 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. import CGMBLEKit
  2. import CGMBLEKitUI
  3. import Combine
  4. import CoreData
  5. import Foundation
  6. import G7SensorKit
  7. import LibreTransmitter
  8. import LoopKit
  9. import LoopKitUI
  10. import Observation
  11. import SwiftDate
  12. import SwiftUI
  13. extension Home {
  14. @Observable final class StateModel: BaseStateModel<Provider> {
  15. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  16. @ObservationIgnored @Injected() var apsManager: APSManager!
  17. @ObservationIgnored @Injected() var pluginCGMManager: PluginManager!
  18. @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
  19. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  20. @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
  21. @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
  22. @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
  23. @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
  24. @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
  25. @ObservationIgnored @Injected() var bluetoothManager: BluetoothStateManager!
  26. @ObservationIgnored @Injected() var iobService: IOBService!
  27. @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
  28. var cgmStateModel: CGMSettings.StateModel {
  29. CGMSettings.StateModel.shared
  30. }
  31. private let timer = DispatchTimer(timeInterval: 30)
  32. private(set) var filteredHours = 24
  33. var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
  34. var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
  35. var manualGlucose: [BloodGlucose] = []
  36. var uploadStats = false
  37. var recentGlucose: BloodGlucose?
  38. var maxBasal: Decimal = 2
  39. var basalProfile: [BasalProfileEntry] = []
  40. var bgTargets = BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
  41. ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
  42. var targetProfiles: [TargetProfile] = []
  43. var timerDate = Date()
  44. var closedLoop = false
  45. var isLooping = false
  46. var statusTitle = ""
  47. var lastLoopDate: Date = .distantPast
  48. var battery: Battery?
  49. var reservoir: Decimal?
  50. var pumpName = ""
  51. var pumpExpiresAtDate: Date?
  52. var pumpActivatedAtDate: Date?
  53. var highTTraisesSens: Bool = false
  54. var lowTTlowersSens: Bool = false
  55. var isExerciseModeActive: Bool = false
  56. var settingHalfBasalTarget: Decimal = 160
  57. var percentage: Int = 100
  58. var shouldDisplayPumpSetupSheet = false
  59. var shouldDisplayCGMSetupSheet = false
  60. var errorMessage: String?
  61. var errorDate: Date?
  62. var bolusProgress: Decimal?
  63. var eventualBG: Int?
  64. var allowManualTemp = false
  65. var units: GlucoseUnits = .mgdL
  66. var pumpDisplayState: PumpDisplayState?
  67. var alarm: GlucoseAlarm?
  68. var manualTempBasal = false
  69. var isSmoothingEnabled = false
  70. var maxIOB: Decimal = 0.0
  71. var currentIOB: Decimal = 0.0
  72. var autosensMax: Decimal = 1.2
  73. var lowGlucose: Decimal = 70
  74. var highGlucose: Decimal = 180
  75. var currentGlucoseTarget: Decimal = 100
  76. var glucoseColorScheme: GlucoseColorScheme = .staticColor
  77. var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
  78. var displayXgridLines: Bool = false
  79. var displayYgridLines: Bool = false
  80. var thresholdLines: Bool = false
  81. var bolusDisplayThreshold: BolusDisplayThreshold = .allUnits
  82. var hours: Int16 = 6
  83. var totalBolus: Decimal = 0
  84. var isLoopStatusPresented: Bool = false
  85. var isLegendPresented: Bool = false
  86. var roundedTotalBolus: String = ""
  87. var selectedTab: Int = 0
  88. var waitForSuggestion: Bool = false
  89. var glucoseFromPersistence: [GlucoseStored] = []
  90. var latestTwoGlucoseValues: [GlucoseStored] = []
  91. var carbsFromPersistence: [CarbEntryStored] = []
  92. var fpusFromPersistence: [CarbEntryStored] = []
  93. var determinationsFromPersistence: [OrefDetermination] = []
  94. var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
  95. var fetchedTDDs: [TDD] = []
  96. var insulinFromPersistence: [PumpEventStored] = []
  97. var tempBasals: [PumpEventStored] = []
  98. var suspendAndResumeEvents: [PumpEventStored] = []
  99. var batteryFromPersistence: [OpenAPS_Battery] = []
  100. var bolusStatus: BolusStatus = .noBolus
  101. var lastPumpBolus: PumpEventStored?
  102. var overrides: [OverrideStored] = []
  103. var overrideRunStored: [OverrideRunStored] = []
  104. var tempTargetStored: [TempTargetStored] = []
  105. var tempTargetRunStored: [TempTargetRunStored] = []
  106. var isOverrideCancelled: Bool = false
  107. var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
  108. var pumpStatusHighlightMessage: String?
  109. var pumpStatusBadgeImage: UIImage?
  110. var pumpStatusBadgeColor: Color?
  111. var cgmAvailable: Bool = false
  112. var cgmDisplayState: CgmDisplayState?
  113. var cgmProgressHighlight: DeviceLifecycleProgress?
  114. var cgmSensorExpiresAt: Date?
  115. var cgmWarmupEndsAt: Date?
  116. var listOfCGM: [CGMModel] = []
  117. var cgmCurrent = cgmDefaultModel
  118. var pumpInitialSettings = PumpConfig.PumpInitialSettings.default
  119. var shouldRunDeleteOnSettingsChange = true
  120. var showCarbsRequiredBadge: Bool = true
  121. var enableQuickBolus: Bool = false
  122. var quickBolusHistory: [Decimal] = []
  123. private(set) var setupPumpType: PumpConfig.PumpType = .minimed
  124. var minForecast: [Int] = []
  125. var maxForecast: [Int] = []
  126. var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
  127. var forecastDisplayType: ForecastDisplayType = .cone
  128. var minYAxisValue: Decimal = 39
  129. var maxYAxisValue: Decimal = 200
  130. var minValueCobChart: Decimal = 0
  131. var maxValueCobChart: Decimal = 20
  132. var minValueIobChart: Decimal = 0
  133. var maxValueIobChart: Decimal = 5
  134. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  135. // MARK: - NSFetchedResultsControllers
  136. //
  137. // Each Core Data backed array on this state model is driven by an NSFetchedResultsController
  138. // bound to the viewContext. The controllers keep their `fetchedObjects` continuously in sync
  139. // with the viewContext (which in turn is fed by the persistent history merge in CoreDataStack)
  140. // and notify us via their delegate's `onContentChange` closure. This replaces the previous
  141. // hand-rolled `changedObjectsOnManagedObjectContextDidSavePublisher` + re-fetch approach.
  142. @ObservationIgnored let glucoseControllerDelegate = FetchedResultsControllerDelegate()
  143. @ObservationIgnored private(set) lazy var glucoseController: NSFetchedResultsController<GlucoseStored> = {
  144. let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
  145. request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: true)]
  146. request.predicate = NSPredicate.glucose
  147. request.fetchBatchSize = 50
  148. let controller = NSFetchedResultsController(
  149. fetchRequest: request,
  150. managedObjectContext: viewContext,
  151. sectionNameKeyPath: nil,
  152. cacheName: nil
  153. )
  154. controller.delegate = glucoseControllerDelegate
  155. return controller
  156. }()
  157. @ObservationIgnored let carbsControllerDelegate = FetchedResultsControllerDelegate()
  158. @ObservationIgnored private(set) lazy var carbsController: NSFetchedResultsController<CarbEntryStored> = {
  159. let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
  160. request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)]
  161. request.predicate = NSPredicate.carbsForChart
  162. request.fetchBatchSize = 5
  163. let controller = NSFetchedResultsController(
  164. fetchRequest: request,
  165. managedObjectContext: viewContext,
  166. sectionNameKeyPath: nil,
  167. cacheName: nil
  168. )
  169. controller.delegate = carbsControllerDelegate
  170. return controller
  171. }()
  172. @ObservationIgnored let fpuControllerDelegate = FetchedResultsControllerDelegate()
  173. @ObservationIgnored private(set) lazy var fpuController: NSFetchedResultsController<CarbEntryStored> = {
  174. let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
  175. request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)]
  176. request.predicate = NSPredicate.fpusForChart
  177. let controller = NSFetchedResultsController(
  178. fetchRequest: request,
  179. managedObjectContext: viewContext,
  180. sectionNameKeyPath: nil,
  181. cacheName: nil
  182. )
  183. controller.delegate = fpuControllerDelegate
  184. return controller
  185. }()
  186. @ObservationIgnored let enactedDeterminationControllerDelegate = FetchedResultsControllerDelegate()
  187. @ObservationIgnored private(set) lazy var enactedDeterminationController: NSFetchedResultsController<OrefDetermination> =
  188. {
  189. let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
  190. request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
  191. request.predicate = NSPredicate.enactedDetermination
  192. request.fetchLimit = 1
  193. let controller = NSFetchedResultsController(
  194. fetchRequest: request,
  195. managedObjectContext: viewContext,
  196. sectionNameKeyPath: nil,
  197. cacheName: nil
  198. )
  199. controller.delegate = enactedDeterminationControllerDelegate
  200. return controller
  201. }()
  202. @ObservationIgnored let determinationControllerDelegate = FetchedResultsControllerDelegate()
  203. @ObservationIgnored private(set) lazy var determinationController: NSFetchedResultsController<OrefDetermination> = {
  204. let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
  205. request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
  206. request.predicate = NSPredicate.determinationsForCobIobCharts
  207. request.fetchBatchSize = 50
  208. let controller = NSFetchedResultsController(
  209. fetchRequest: request,
  210. managedObjectContext: viewContext,
  211. sectionNameKeyPath: nil,
  212. cacheName: nil
  213. )
  214. controller.delegate = determinationControllerDelegate
  215. return controller
  216. }()
  217. @ObservationIgnored let insulinControllerDelegate = FetchedResultsControllerDelegate()
  218. @ObservationIgnored private(set) lazy var insulinController: NSFetchedResultsController<PumpEventStored> = {
  219. let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
  220. request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: true)]
  221. request.predicate = NSPredicate.pumpHistoryLast24h
  222. request.fetchBatchSize = 30
  223. let controller = NSFetchedResultsController(
  224. fetchRequest: request,
  225. managedObjectContext: viewContext,
  226. sectionNameKeyPath: nil,
  227. cacheName: nil
  228. )
  229. controller.delegate = insulinControllerDelegate
  230. return controller
  231. }()
  232. @ObservationIgnored let lastBolusControllerDelegate = FetchedResultsControllerDelegate()
  233. @ObservationIgnored private(set) lazy var lastBolusController: NSFetchedResultsController<PumpEventStored> = {
  234. let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
  235. request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: false)]
  236. request.predicate = NSPredicate.lastPumpBolus
  237. request.fetchLimit = 1
  238. let controller = NSFetchedResultsController(
  239. fetchRequest: request,
  240. managedObjectContext: viewContext,
  241. sectionNameKeyPath: nil,
  242. cacheName: nil
  243. )
  244. controller.delegate = lastBolusControllerDelegate
  245. return controller
  246. }()
  247. @ObservationIgnored let overrideControllerDelegate = FetchedResultsControllerDelegate()
  248. @ObservationIgnored private(set) lazy var overrideController: NSFetchedResultsController<OverrideStored> = {
  249. let request = NSFetchRequest<OverrideStored>(entityName: "OverrideStored")
  250. request.sortDescriptors = [NSSortDescriptor(keyPath: \OverrideStored.date, ascending: false)]
  251. request.predicate = NSPredicate.lastActiveOverride
  252. let controller = NSFetchedResultsController(
  253. fetchRequest: request,
  254. managedObjectContext: viewContext,
  255. sectionNameKeyPath: nil,
  256. cacheName: nil
  257. )
  258. controller.delegate = overrideControllerDelegate
  259. return controller
  260. }()
  261. @ObservationIgnored let overrideRunControllerDelegate = FetchedResultsControllerDelegate()
  262. @ObservationIgnored private(set) lazy var overrideRunController: NSFetchedResultsController<OverrideRunStored> = {
  263. let request = NSFetchRequest<OverrideRunStored>(entityName: "OverrideRunStored")
  264. request.sortDescriptors = [NSSortDescriptor(keyPath: \OverrideRunStored.startDate, ascending: false)]
  265. request.predicate = NSPredicate.predicateForStartDateOneDayAgo
  266. let controller = NSFetchedResultsController(
  267. fetchRequest: request,
  268. managedObjectContext: viewContext,
  269. sectionNameKeyPath: nil,
  270. cacheName: nil
  271. )
  272. controller.delegate = overrideRunControllerDelegate
  273. return controller
  274. }()
  275. @ObservationIgnored let tempTargetControllerDelegate = FetchedResultsControllerDelegate()
  276. @ObservationIgnored private(set) lazy var tempTargetController: NSFetchedResultsController<TempTargetStored> = {
  277. let request = NSFetchRequest<TempTargetStored>(entityName: "TempTargetStored")
  278. request.sortDescriptors = [NSSortDescriptor(keyPath: \TempTargetStored.date, ascending: false)]
  279. request.predicate = NSPredicate.tempTargetsForMainChart
  280. let controller = NSFetchedResultsController(
  281. fetchRequest: request,
  282. managedObjectContext: viewContext,
  283. sectionNameKeyPath: nil,
  284. cacheName: nil
  285. )
  286. controller.delegate = tempTargetControllerDelegate
  287. return controller
  288. }()
  289. @ObservationIgnored let tempTargetRunControllerDelegate = FetchedResultsControllerDelegate()
  290. @ObservationIgnored private(set) lazy var tempTargetRunController: NSFetchedResultsController<TempTargetRunStored> = {
  291. let request = NSFetchRequest<TempTargetRunStored>(entityName: "TempTargetRunStored")
  292. request.sortDescriptors = [NSSortDescriptor(keyPath: \TempTargetRunStored.startDate, ascending: false)]
  293. request.predicate = NSPredicate.predicateForStartDateOneDayAgo
  294. let controller = NSFetchedResultsController(
  295. fetchRequest: request,
  296. managedObjectContext: viewContext,
  297. sectionNameKeyPath: nil,
  298. cacheName: nil
  299. )
  300. controller.delegate = tempTargetRunControllerDelegate
  301. return controller
  302. }()
  303. @ObservationIgnored let batteryControllerDelegate = FetchedResultsControllerDelegate()
  304. @ObservationIgnored private(set) lazy var batteryController: NSFetchedResultsController<OpenAPS_Battery> = {
  305. let request = NSFetchRequest<OpenAPS_Battery>(entityName: "OpenAPS_Battery")
  306. request.sortDescriptors = [NSSortDescriptor(keyPath: \OpenAPS_Battery.date, ascending: false)]
  307. request.predicate = NSPredicate.predicateFor30MinAgo
  308. let controller = NSFetchedResultsController(
  309. fetchRequest: request,
  310. managedObjectContext: viewContext,
  311. sectionNameKeyPath: nil,
  312. cacheName: nil
  313. )
  314. controller.delegate = batteryControllerDelegate
  315. return controller
  316. }()
  317. @ObservationIgnored let tddControllerDelegate = FetchedResultsControllerDelegate()
  318. @ObservationIgnored private(set) lazy var tddController: NSFetchedResultsController<TDDStored> = {
  319. let request = NSFetchRequest<TDDStored>(entityName: "TDDStored")
  320. request.sortDescriptors = [NSSortDescriptor(keyPath: \TDDStored.date, ascending: false)]
  321. request.predicate = NSPredicate.predicateForOneDayAgo
  322. request.fetchLimit = 1
  323. let controller = NSFetchedResultsController(
  324. fetchRequest: request,
  325. managedObjectContext: viewContext,
  326. sectionNameKeyPath: nil,
  327. cacheName: nil
  328. )
  329. controller.delegate = tddControllerDelegate
  330. return controller
  331. }()
  332. // MARK: - Fetch window re-anchoring
  333. //
  334. // Window predicates freeze their anchor at build time, so the unbounded controllers
  335. // are re-anchored to "now" on foreground. fetchLimit-1 and override controllers keep
  336. // the launch anchor on purpose; temp targets must re-anchor ("date >= now" disjunct).
  337. /// Called on `willEnterForegroundNotification`; idempotent at launch.
  338. @MainActor func reanchorFetchWindows() {
  339. reanchor(glucoseController, with: NSPredicate.glucose) {
  340. self.updateGlucoseFromController()
  341. // Re-sync the chart domain even if no new reading arrived while backgrounded.
  342. self.updateStartEndMarkers()
  343. }
  344. reanchor(carbsController, with: NSPredicate.carbsForChart) { self.updateCarbsFromController() }
  345. reanchor(fpuController, with: NSPredicate.fpusForChart) { self.updateFPUsFromController() }
  346. reanchor(determinationController, with: NSPredicate.determinationsForCobIobCharts) {
  347. self.updateDeterminationsFromController()
  348. }
  349. reanchor(insulinController, with: NSPredicate.pumpHistoryLast24h) { self.updateInsulinFromController() }
  350. reanchor(overrideRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
  351. self.updateOverrideRunsFromController()
  352. }
  353. reanchor(tempTargetController, with: NSPredicate.tempTargetsForMainChart) {
  354. self.updateTempTargetsFromController()
  355. }
  356. reanchor(tempTargetRunController, with: NSPredicate.predicateForStartDateOneDayAgo) {
  357. self.updateTempTargetRunsFromController()
  358. }
  359. reanchor(batteryController, with: NSPredicate.predicateFor30MinAgo) { self.updateBatteryFromController() }
  360. }
  361. @MainActor private func reanchor<T: NSFetchRequestResult>(
  362. _ controller: NSFetchedResultsController<T>,
  363. with predicate: NSPredicate,
  364. republish: () -> Void
  365. ) {
  366. controller.fetchRequest.predicate = predicate
  367. do {
  368. try controller.performFetch()
  369. republish()
  370. } catch {
  371. debug(.default, "\(DebuggingIdentifiers.failed) Failed to re-anchor fetch window for \(T.self): \(error)")
  372. }
  373. }
  374. private var subscriptions = Set<AnyCancellable>()
  375. /// Debounces the forecast recompute — the most expensive `onContentChange` callback,
  376. /// which a manual re-determine fires twice in quick succession.
  377. @ObservationIgnored var forecastUpdateTask: Task<Void, Never>?
  378. typealias PumpEvent = PumpEventStored.EventType
  379. override init() {
  380. super.init()
  381. }
  382. override func subscribe() {
  383. registerSubscribers()
  384. // Parallelize Setup functions
  385. setupHomeViewConcurrently()
  386. }
  387. private func setupHomeViewConcurrently() {
  388. Task {
  389. // We need to initialize settings and observers first
  390. await self.setupSettings()
  391. await self.setupPumpSettings()
  392. await self.setupCGMSettings()
  393. self.registerObservers()
  394. // Set up the NSFetchedResultsControllers. These are bound to the viewContext,
  395. // so `performFetch` and the initial population must run on the main actor.
  396. await self.setupGlucoseController()
  397. await self.setupCarbsController()
  398. await self.setupFPUController()
  399. await self.setupEnactedDeterminationController()
  400. await self.setupDeterminationController()
  401. await self.setupInsulinController()
  402. await self.setupLastBolusController()
  403. await self.setupOverrideController()
  404. await self.setupOverrideRunController()
  405. await self.setupTempTargetController()
  406. await self.setupTempTargetRunController()
  407. await self.setupBatteryController()
  408. await self.setupTDDController()
  409. // The rest can be initialized concurrently
  410. await withTaskGroup(of: Void.self) { group in
  411. group.addTask {
  412. await self.setupBasalProfile()
  413. }
  414. group.addTask {
  415. await self.setupGlucoseTargets()
  416. }
  417. group.addTask {
  418. self.setupReservoir()
  419. }
  420. group.addTask {
  421. self.iobService.updateIOB()
  422. }
  423. }
  424. }
  425. }
  426. private func registerSubscribers() {
  427. iobService.iobPublisher
  428. .receive(on: DispatchQueue.main)
  429. .sink { [weak self] _ in
  430. guard let self = self else { return }
  431. self.currentIOB = self.iobService.currentIOB ?? 0
  432. }
  433. .store(in: &subscriptions)
  434. // core-data-fixes drives Core Data updates via NSFetchedResultsController delegates,
  435. // so dev's glucose/carbs updatePublisher sinks and the coreDataPublisher-based
  436. // registerHandlers() are obsolete here. Only the bolus-status subscription (a genuinely
  437. // new feature, consumed by HomeRootView) is carried over, wired in our subscriber style.
  438. provider.deviceManager.bolusTrigger
  439. .receive(on: DispatchQueue.main)
  440. .weakAssign(to: \.bolusStatus, on: self)
  441. .store(in: &subscriptions)
  442. }
  443. private func registerObservers() {
  444. broadcaster.register(DeterminationObserver.self, observer: self)
  445. broadcaster.register(SettingsObserver.self, observer: self)
  446. broadcaster.register(PreferencesObserver.self, observer: self)
  447. broadcaster.register(PumpSettingsObserver.self, observer: self)
  448. broadcaster.register(BasalProfileObserver.self, observer: self)
  449. broadcaster.register(BGTargetsObserver.self, observer: self)
  450. broadcaster.register(PumpReservoirObserver.self, observer: self)
  451. broadcaster.register(PumpDeactivatedObserver.self, observer: self)
  452. timer.eventHandler = {
  453. DispatchQueue.main.async { [weak self] in
  454. guard let self else { return }
  455. self.timerDate = Date()
  456. // The publisher only re-emits on state changes; re-pull
  457. // so the arc + countdowns + status text advance during
  458. // warmup / stabilizing / expiry. Simulator has no
  459. // CGMManager, so fall back to reading its synthetic
  460. // lifecycle / highlight so the bobble sees the same
  461. // data shape a real CGM would deliver.
  462. let manager = self.fetchGlucoseManager.cgmManager
  463. let source = self.fetchGlucoseManager.glucoseSource
  464. let progress: DeviceLifecycleProgress?
  465. let highlight: DeviceStatusHighlight?
  466. if let manager {
  467. progress = manager.cgmLifecycleProgress
  468. highlight = manager.cgmStatusHighlight
  469. } else if let sim = source as? GlucoseSimulatorSource {
  470. progress = sim.cgmLifecycleProgress
  471. highlight = sim.cgmStatusHighlight
  472. } else {
  473. progress = nil
  474. highlight = nil
  475. }
  476. self.cgmProgressHighlight = progress
  477. if let highlight {
  478. self.cgmDisplayState = CgmDisplayState(
  479. localizedMessage: highlight.localizedMessage,
  480. imageName: highlight.imageName,
  481. status: CgmDisplayStatus.from(highlight.state)
  482. )
  483. } else {
  484. self.cgmDisplayState = nil
  485. }
  486. self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
  487. manager: manager,
  488. glucoseSource: source,
  489. lifecycle: progress
  490. )
  491. self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(manager: manager)
  492. }
  493. }
  494. timer.resume()
  495. Foundation.NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
  496. .sink { [weak self] _ in
  497. Task { @MainActor in
  498. self?.reanchorFetchWindows()
  499. }
  500. }
  501. .store(in: &lifetime)
  502. fetchGlucoseManager.cgmDisplayState
  503. .receive(on: DispatchQueue.main)
  504. .sink { [weak self] state in self?.cgmDisplayState = state }
  505. .store(in: &lifetime)
  506. fetchGlucoseManager.cgmProgressHighlight
  507. .receive(on: DispatchQueue.main)
  508. .sink { [weak self] progress in
  509. guard let self else { return }
  510. self.cgmProgressHighlight = progress
  511. self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
  512. manager: self.fetchGlucoseManager.cgmManager,
  513. glucoseSource: self.fetchGlucoseManager.glucoseSource,
  514. lifecycle: progress
  515. )
  516. self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(
  517. manager: self.fetchGlucoseManager.cgmManager
  518. )
  519. }
  520. .store(in: &lifetime)
  521. apsManager.isLooping
  522. .receive(on: DispatchQueue.main)
  523. .weakAssign(to: \.isLooping, on: self)
  524. .store(in: &lifetime)
  525. apsManager.lastLoopDateSubject
  526. .receive(on: DispatchQueue.main)
  527. .weakAssign(to: \.lastLoopDate, on: self)
  528. .store(in: &lifetime)
  529. apsManager.pumpName
  530. .receive(on: DispatchQueue.main)
  531. .weakAssign(to: \.pumpName, on: self)
  532. .store(in: &lifetime)
  533. apsManager.pumpExpiresAtDate
  534. .receive(on: DispatchQueue.main)
  535. .weakAssign(to: \.pumpExpiresAtDate, on: self)
  536. .store(in: &lifetime)
  537. apsManager.pumpActivatedAtDate
  538. .receive(on: DispatchQueue.main)
  539. .weakAssign(to: \.pumpActivatedAtDate, on: self)
  540. .store(in: &lifetime)
  541. apsManager.lastError
  542. .receive(on: DispatchQueue.main)
  543. .map { [weak self] error in
  544. self?.errorDate = error == nil ? nil : Date()
  545. if let error = error {
  546. debug(.default, "APSManager lastError: \(String(describing: error))")
  547. }
  548. return error?.localizedDescription
  549. }
  550. .weakAssign(to: \.errorMessage, on: self)
  551. .store(in: &lifetime)
  552. apsManager.bolusProgress
  553. .receive(on: DispatchQueue.main)
  554. .weakAssign(to: \.bolusProgress, on: self)
  555. .store(in: &lifetime)
  556. apsManager.pumpDisplayState
  557. .receive(on: DispatchQueue.main)
  558. .sink { [weak self] state in
  559. guard let self = self else { return }
  560. self.pumpDisplayState = state
  561. if state == nil {
  562. self.reservoir = nil
  563. self.battery = nil
  564. self.pumpName = ""
  565. self.pumpExpiresAtDate = nil
  566. self.shouldDisplayPumpSetupSheet = false
  567. } else {
  568. self.setupReservoir()
  569. self.displayPumpStatusHighlightMessage()
  570. self.displayPumpStatusBadge()
  571. self.setupBatteryArray()
  572. }
  573. }
  574. .store(in: &lifetime)
  575. }
  576. private enum SettingType {
  577. case basal
  578. case carbRatio
  579. case bgTarget
  580. case isf
  581. }
  582. @MainActor private func setupSettings() async {
  583. units = settingsManager.settings.units
  584. allowManualTemp = !settingsManager.settings.closedLoop
  585. closedLoop = settingsManager.settings.closedLoop
  586. lastLoopDate = apsManager.lastLoopDate
  587. alarm = provider.glucoseStorage.alarm
  588. manualTempBasal = apsManager.isManualTempBasal
  589. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  590. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  591. autosensMax = settingsManager.preferences.autosensMax
  592. lowGlucose = settingsManager.settings.low
  593. highGlucose = settingsManager.settings.high
  594. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  595. displayXgridLines = settingsManager.settings.xGridLines
  596. displayYgridLines = settingsManager.settings.yGridLines
  597. bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
  598. thresholdLines = settingsManager.settings.rulerMarks
  599. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  600. enableQuickBolus = settingsManager.settings.enableQuickBolus
  601. forecastDisplayType = settingsManager.settings.forecastDisplayType
  602. isExerciseModeActive = settingsManager.preferences.exerciseMode
  603. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  604. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  605. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  606. maxIOB = settingsManager.preferences.maxIOB
  607. }
  608. @MainActor private func setupCGMSettings() async {
  609. cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
  610. listOfCGM = (
  611. CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  612. CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  613. } +
  614. pluginCGMManager.availableCGMManagers.map {
  615. CGMModel(
  616. id: $0.identifier,
  617. type: CGMType.plugin,
  618. displayName: $0.localizedTitle,
  619. subtitle: $0.localizedTitle
  620. )
  621. }
  622. ).sorted(by: { lhs, rhs in
  623. if lhs.displayName == "None" {
  624. return true
  625. } else if rhs.displayName == "None" {
  626. return false
  627. } else {
  628. return lhs.displayName < rhs.displayName
  629. }
  630. })
  631. switch settingsManager.settings.cgm {
  632. case .plugin:
  633. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  634. cgmCurrent = CGMModel(
  635. id: settingsManager.settings.cgmPluginIdentifier,
  636. type: .plugin,
  637. displayName: cgmPluginInfo.displayName,
  638. subtitle: cgmPluginInfo.subtitle
  639. )
  640. } else {
  641. // no more type of plugin available - fallback to default
  642. cgmCurrent = cgmDefaultModel
  643. }
  644. default:
  645. cgmCurrent = CGMModel(
  646. id: settingsManager.settings.cgm.id,
  647. type: settingsManager.settings.cgm,
  648. displayName: settingsManager.settings.cgm.displayName,
  649. subtitle: settingsManager.settings.cgm.subtitle
  650. )
  651. }
  652. }
  653. func loadQuickBolusSuggestions() async {
  654. guard enableQuickBolus else { return }
  655. let fetchContext = CoreDataStack.shared.newTaskContext()
  656. let cutoff = Calendar.current.date(byAdding: .day, value: -90, to: Date()) ?? Date()
  657. let predicate = NSPredicate(
  658. format: "isSMB == false AND isExternal == false AND pumpEvent.timestamp >= %@",
  659. cutoff as NSDate
  660. )
  661. do {
  662. let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
  663. ofType: BolusStored.self,
  664. onContext: fetchContext,
  665. predicate: predicate,
  666. key: "pumpEvent.timestamp",
  667. ascending: false,
  668. batchSize: 100
  669. )
  670. let suggestions: [Decimal] = await fetchContext.perform {
  671. guard let boluses = results as? [BolusStored] else { return [] }
  672. let now = Date()
  673. let cal = Calendar.current
  674. let nowMinute = cal.component(.hour, from: now) * 60 + cal.component(.minute, from: now)
  675. let nowDOW = cal.component(.weekday, from: now)
  676. let sigma: Double = 60.0
  677. let halfLife: Double = 10.0
  678. var groups: [Decimal: Double] = [:]
  679. for bolus in boluses {
  680. guard let nsAmount = bolus.amount, nsAmount.doubleValue > 0,
  681. let timestamp = bolus.pumpEvent?.timestamp else { continue }
  682. var roundedKey = Decimal()
  683. var tempAmount = nsAmount as Decimal
  684. NSDecimalRound(&roundedKey, &tempAmount, 2, .plain)
  685. let entryMinute = cal.component(.hour, from: timestamp) * 60 + cal.component(.minute, from: timestamp)
  686. let entryDOW = cal.component(.weekday, from: timestamp)
  687. let diff = abs(entryMinute - nowMinute)
  688. let circularDiff = Double(min(diff, 1440 - diff))
  689. let t = exp(-(circularDiff * circularDiff) / (2.0 * sigma * sigma))
  690. let d: Double
  691. if entryDOW == nowDOW {
  692. d = 1.0
  693. } else {
  694. let nowWeekend = nowDOW == 1 || nowDOW == 7
  695. let entryWeekend = entryDOW == 1 || entryDOW == 7
  696. d = nowWeekend == entryWeekend ? 0.7 : 0.15
  697. }
  698. let daysAgo = now.timeIntervalSince(timestamp) / 86400.0
  699. let r = pow(0.5, daysAgo / halfLife)
  700. groups[roundedKey, default: 0] += t * d * r
  701. }
  702. return groups
  703. .filter { $0.value >= 0.1 }
  704. .sorted { $0.value > $1.value }
  705. .prefix(5)
  706. .map(\.key)
  707. }
  708. await MainActor.run {
  709. quickBolusHistory = suggestions
  710. }
  711. } catch {
  712. debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch quick bolus history: \(error)")
  713. }
  714. }
  715. func enactQuickBolus(amount: Decimal) async -> Bool {
  716. guard amount > 0 else { return false }
  717. let delivery = min(
  718. Double(truncating: amount as NSDecimalNumber),
  719. pumpInitialSettings.maxBolusUnits
  720. )
  721. do {
  722. let authenticated = try await unlockmanager.unlock()
  723. if authenticated {
  724. await apsManager.enactBolus(amount: delivery, isSMB: false, callback: nil)
  725. return true
  726. }
  727. return false
  728. } catch {
  729. debug(.bolusState, "Quick bolus authentication error: \(error)")
  730. return false
  731. }
  732. }
  733. func addPump(_ type: PumpConfig.PumpType) {
  734. setupPumpType = type
  735. shouldDisplayPumpSetupSheet = true
  736. }
  737. func addCGM(cgm: CGMModel) {
  738. cgmCurrent = cgm
  739. switch cgmCurrent.type {
  740. case .plugin:
  741. shouldDisplayCGMSetupSheet = true
  742. default:
  743. shouldDisplayCGMSetupSheet = true
  744. settingsManager.settings.cgm = cgmCurrent.type
  745. settingsManager.settings.cgmPluginIdentifier = ""
  746. fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  747. broadcaster.notify(GlucoseObserver.self, on: .main) {
  748. $0.glucoseDidUpdate([])
  749. }
  750. }
  751. }
  752. func deleteCGM() {
  753. fetchGlucoseManager.performOnCGMManagerQueue {
  754. // Call plugin functionality on the manager queue (or at least attempt to)
  755. Task {
  756. await self.fetchGlucoseManager?.deleteGlucoseSource()
  757. // UI updates go back to Main
  758. await MainActor.run {
  759. self.shouldDisplayCGMSetupSheet = false
  760. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  761. $0.glucoseDidUpdate([])
  762. }
  763. }
  764. }
  765. }
  766. }
  767. /// Display the eventual status message provided by the manager of the pump
  768. /// Only display if state is warning or critical message else return nil
  769. func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
  770. DispatchQueue.main.async { [weak self] in
  771. guard let self = self else { return }
  772. if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
  773. statusHighlight.state == .warning || statusHighlight.state == .critical, !didDeactivate
  774. {
  775. pumpStatusHighlightMessage = (statusHighlight.state == .warning ? "⚠️\n" : "‼️\n") + statusHighlight
  776. .localizedMessage
  777. } else {
  778. pumpStatusHighlightMessage = nil
  779. }
  780. }
  781. }
  782. func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
  783. DispatchQueue.main.async { [weak self] in
  784. guard let self = self else { return }
  785. if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,
  786. let image = statusBadge.image, !didDeactivate
  787. {
  788. pumpStatusBadgeImage = image
  789. pumpStatusBadgeColor = statusBadge.state == .critical ? .critical : .warning
  790. } else {
  791. pumpStatusBadgeImage = nil
  792. pumpStatusBadgeColor = nil
  793. }
  794. }
  795. }
  796. func runLoop() {
  797. provider.heartbeatNow()
  798. }
  799. func showProgressView() {
  800. glucoseStorage
  801. .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
  802. }
  803. func cancelBolus() {
  804. Task {
  805. await apsManager.cancelBolus(nil)
  806. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  807. try await apsManager.determineBasalSync()
  808. }
  809. }
  810. private func setupPumpSettings() async {
  811. let settings = await provider.pumpSettings()
  812. await MainActor.run {
  813. self.maxBasal = settings.maxBasal
  814. self.pumpInitialSettings.maxBasalRateUnitsPerHour = Double(settings.maxBasal)
  815. self.pumpInitialSettings.maxBolusUnits = Double(settings.maxBolus)
  816. }
  817. }
  818. private func setupBasalProfile() async {
  819. let basalProfile = await provider.getBasalProfile()
  820. await MainActor.run {
  821. self.basalProfile = basalProfile
  822. if let schedule = BasalRateSchedule(
  823. dailyItems: basalProfile
  824. .map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) }
  825. ) {
  826. self.pumpInitialSettings.basalSchedule = schedule
  827. }
  828. }
  829. }
  830. private func setupGlucoseTargets() async {
  831. let bgTargets = await provider.getBGTargets()
  832. let targetProfiles = processFetchedTargets(bgTargets, startMarker: startMarker)
  833. await MainActor.run {
  834. self.bgTargets = bgTargets
  835. self.targetProfiles = targetProfiles
  836. }
  837. }
  838. private func setupReservoir() {
  839. Task {
  840. let reservoir = await provider.pumpReservoir()
  841. await MainActor.run {
  842. self.reservoir = reservoir
  843. }
  844. }
  845. }
  846. private func getCurrentGlucoseTarget() async {
  847. let now = Date()
  848. let calendar = Calendar.current
  849. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  850. for (index, entry) in entries.enumerated() {
  851. guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
  852. debug(.default, "Invalid entry start time: \(entry.start)")
  853. continue
  854. }
  855. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  856. let entryStartTime = calendar.date(
  857. bySettingHour: entryComponents.hour!,
  858. minute: entryComponents.minute!,
  859. second: entryComponents.second!,
  860. of: now
  861. )!
  862. let entryEndTime: Date
  863. if index < entries.count - 1,
  864. let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
  865. {
  866. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  867. entryEndTime = calendar.date(
  868. bySettingHour: nextEntryComponents.hour!,
  869. minute: nextEntryComponents.minute!,
  870. second: nextEntryComponents.second!,
  871. of: now
  872. )!
  873. } else {
  874. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  875. }
  876. if now >= entryStartTime, now < entryEndTime {
  877. await MainActor.run {
  878. currentGlucoseTarget = entry.value
  879. }
  880. return
  881. }
  882. }
  883. }
  884. /// Sensor expiration for the home label. Prefers manager-reported
  885. /// dates; reverse-derives from `lifecycle.percentComplete` when not.
  886. /// `activatedAt` must be session start, not transmitter activation.
  887. private static func resolveSensorExpiresAt(
  888. manager: CGMManagerUI?,
  889. glucoseSource: GlucoseSource?,
  890. lifecycle: DeviceLifecycleProgress?
  891. ) -> Date? {
  892. if let sim = glucoseSource as? GlucoseSimulatorSource {
  893. return sim.simulatedSensorExpiresAt
  894. }
  895. guard let manager else { return nil }
  896. // Once a G7 enters grace period, `sensorExpiresAt` is in the past
  897. // and would collapse the bobble countdown to "<1m" while the arc
  898. // (driven by lifecycle.percentComplete against `sensorEndsAt`) is
  899. // still mid-progress. Fall back to `sensorEndsAt` so bobble and
  900. // arc agree, and the user sees grace-period time remaining.
  901. if let g7 = manager as? G7CGMManager {
  902. let now = Date()
  903. if let exp = g7.sensorExpiresAt, exp > now { return exp }
  904. return g7.sensorEndsAt ?? g7.sensorExpiresAt
  905. }
  906. if let g6 = manager as? G6CGMManager, let exp = g6.latestReading?.sessionExpDate { return exp }
  907. if let g5 = manager as? G5CGMManager, let exp = g5.latestReading?.sessionExpDate { return exp }
  908. let activatedAt: Date?
  909. if let g7 = manager as? G7CGMManager {
  910. activatedAt = g7.sensorActivatedAt
  911. } else if let libre = manager as? LibreTransmitterManagerV3 {
  912. activatedAt = libre.sensorInfoObservable.activatedAt
  913. } else {
  914. activatedAt = nil
  915. }
  916. guard let activatedAt,
  917. let lifecycle,
  918. lifecycle.percentComplete > 0.001
  919. else { return nil }
  920. let elapsed = Date().timeIntervalSince(activatedAt)
  921. guard elapsed > 0 else { return nil }
  922. return activatedAt.addingTimeInterval(elapsed / lifecycle.percentComplete)
  923. }
  924. /// Wall-clock end of the sensor's warmup window; `nil` when not warming up.
  925. private static func resolveWarmupEndsAt(manager: CGMManagerUI?) -> Date? {
  926. guard let manager else { return nil }
  927. if let g7 = manager as? G7CGMManager {
  928. guard let ends = g7.sensorFinishesWarmupAt, ends > Date() else { return nil }
  929. return ends
  930. }
  931. if let g6 = manager as? G6CGMManager, let start = g6.latestReading?.sessionStartDate {
  932. let window: TimeInterval = g6.isAnubis ? 50 * 60 : 2 * 60 * 60
  933. let ends = start.addingTimeInterval(window)
  934. return ends > Date() ? ends : nil
  935. }
  936. if let g5 = manager as? G5CGMManager, let start = g5.latestReading?.sessionStartDate {
  937. let ends = start.addingTimeInterval(2 * 60 * 60)
  938. return ends > Date() ? ends : nil
  939. }
  940. return nil
  941. }
  942. }
  943. }
  944. extension Home.StateModel:
  945. DeterminationObserver,
  946. SettingsObserver,
  947. PreferencesObserver,
  948. PumpSettingsObserver,
  949. BasalProfileObserver,
  950. BGTargetsObserver,
  951. PumpReservoirObserver,
  952. PumpDeactivatedObserver
  953. {
  954. func determinationDidUpdate(_: Determination) {
  955. waitForSuggestion = false
  956. }
  957. func settingsDidChange(_ settings: TrioSettings) {
  958. allowManualTemp = !settings.closedLoop
  959. closedLoop = settingsManager.settings.closedLoop
  960. units = settingsManager.settings.units
  961. manualTempBasal = apsManager.isManualTempBasal
  962. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  963. lowGlucose = settingsManager.settings.low
  964. highGlucose = settingsManager.settings.high
  965. Task {
  966. await getCurrentGlucoseTarget()
  967. await setupGlucoseTargets()
  968. }
  969. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  970. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  971. displayXgridLines = settingsManager.settings.xGridLines
  972. displayYgridLines = settingsManager.settings.yGridLines
  973. thresholdLines = settingsManager.settings.rulerMarks
  974. bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
  975. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  976. enableQuickBolus = settingsManager.settings.enableQuickBolus
  977. forecastDisplayType = settingsManager.settings.forecastDisplayType
  978. cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
  979. displayPumpStatusHighlightMessage()
  980. displayPumpStatusBadge()
  981. setupBatteryArray()
  982. Task {
  983. await setupCGMSettings()
  984. }
  985. if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
  986. shouldRunDeleteOnSettingsChange = false
  987. cgmCurrent = cgmDefaultModel
  988. DispatchQueue.main.async {
  989. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  990. $0.glucoseDidUpdate([])
  991. }
  992. }
  993. } else {
  994. shouldRunDeleteOnSettingsChange = true
  995. }
  996. }
  997. func preferencesDidChange(_: Preferences) {
  998. autosensMax = settingsManager.preferences.autosensMax
  999. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  1000. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  1001. isExerciseModeActive = settingsManager.preferences.exerciseMode
  1002. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  1003. maxIOB = settingsManager.preferences.maxIOB
  1004. }
  1005. func pumpSettingsDidChange(_: PumpSettings) {
  1006. Task {
  1007. await setupPumpSettings()
  1008. setupBatteryArray()
  1009. }
  1010. }
  1011. func basalProfileDidChange(_: [BasalProfileEntry]) {
  1012. Task {
  1013. await setupBasalProfile()
  1014. }
  1015. }
  1016. func bgTargetsDidChange(_: BGTargets) {
  1017. Task {
  1018. await setupGlucoseTargets()
  1019. }
  1020. }
  1021. func pumpReservoirDidChange(_: Decimal) {
  1022. setupReservoir()
  1023. displayPumpStatusHighlightMessage()
  1024. displayPumpStatusBadge()
  1025. }
  1026. func pumpDeactivatedDidChange() {
  1027. displayPumpStatusHighlightMessage(true)
  1028. displayPumpStatusBadge(true)
  1029. batteryFromPersistence = []
  1030. }
  1031. }
  1032. extension Home.StateModel: PumpManagerOnboardingDelegate {
  1033. func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
  1034. provider.apsManager.pumpManager = pumpManager
  1035. if let insulinType = pumpManager.status.insulinType {
  1036. settingsManager.updateInsulinCurve(insulinType)
  1037. }
  1038. }
  1039. func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
  1040. // nothing to do
  1041. }
  1042. func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
  1043. // nothing to do
  1044. }
  1045. }