HomeStateModel.swift 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  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. var cgmStateModel: CGMSettings.StateModel {
  28. CGMSettings.StateModel.shared
  29. }
  30. private let timer = DispatchTimer(timeInterval: 30)
  31. private(set) var filteredHours = 24
  32. var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
  33. var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
  34. var manualGlucose: [BloodGlucose] = []
  35. var uploadStats = false
  36. var recentGlucose: BloodGlucose?
  37. var maxBasal: Decimal = 2
  38. var basalProfile: [BasalProfileEntry] = []
  39. var bgTargets = BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
  40. ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
  41. var targetProfiles: [TargetProfile] = []
  42. var timerDate = Date()
  43. var closedLoop = false
  44. var isLooping = false
  45. var statusTitle = ""
  46. var lastLoopDate: Date = .distantPast
  47. var battery: Battery?
  48. var reservoir: Decimal?
  49. var pumpName = ""
  50. var pumpExpiresAtDate: Date?
  51. var pumpActivatedAtDate: Date?
  52. var highTTraisesSens: Bool = false
  53. var lowTTlowersSens: Bool = false
  54. var isExerciseModeActive: Bool = false
  55. var settingHalfBasalTarget: Decimal = 160
  56. var percentage: Int = 100
  57. var shouldDisplayPumpSetupSheet = false
  58. var shouldDisplayCGMSetupSheet = false
  59. var errorMessage: String?
  60. var errorDate: Date?
  61. var bolusProgress: Decimal?
  62. var eventualBG: Int?
  63. var allowManualTemp = false
  64. var units: GlucoseUnits = .mgdL
  65. var pumpDisplayState: PumpDisplayState?
  66. var alarm: GlucoseAlarm?
  67. var manualTempBasal = false
  68. var isSmoothingEnabled = false
  69. var maxIOB: Decimal = 0.0
  70. var currentIOB: Decimal = 0.0
  71. var autosensMax: Decimal = 1.2
  72. var lowGlucose: Decimal = 70
  73. var highGlucose: Decimal = 180
  74. var currentGlucoseTarget: Decimal = 100
  75. var glucoseColorScheme: GlucoseColorScheme = .staticColor
  76. var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
  77. var displayXgridLines: Bool = false
  78. var displayYgridLines: Bool = false
  79. var thresholdLines: Bool = false
  80. var bolusDisplayThreshold: BolusDisplayThreshold = .allUnits
  81. var hours: Int16 = 6
  82. var totalBolus: Decimal = 0
  83. var isLoopStatusPresented: Bool = false
  84. var isLegendPresented: Bool = false
  85. var roundedTotalBolus: String = ""
  86. var selectedTab: Int = 0
  87. var waitForSuggestion: Bool = false
  88. var glucoseFromPersistence: [GlucoseStored] = []
  89. var latestTwoGlucoseValues: [GlucoseStored] = []
  90. var carbsFromPersistence: [CarbEntryStored] = []
  91. var fpusFromPersistence: [CarbEntryStored] = []
  92. var determinationsFromPersistence: [OrefDetermination] = []
  93. var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
  94. var fetchedTDDs: [TDD] = []
  95. var insulinFromPersistence: [PumpEventStored] = []
  96. var tempBasals: [PumpEventStored] = []
  97. var suspendAndResumeEvents: [PumpEventStored] = []
  98. var batteryFromPersistence: [OpenAPS_Battery] = []
  99. var lastPumpBolus: PumpEventStored?
  100. var overrides: [OverrideStored] = []
  101. var overrideRunStored: [OverrideRunStored] = []
  102. var tempTargetStored: [TempTargetStored] = []
  103. var tempTargetRunStored: [TempTargetRunStored] = []
  104. var isOverrideCancelled: Bool = false
  105. var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
  106. var pumpStatusHighlightMessage: String?
  107. var pumpStatusBadgeImage: UIImage?
  108. var pumpStatusBadgeColor: Color?
  109. var cgmAvailable: Bool = false
  110. var cgmDisplayState: CgmDisplayState?
  111. var cgmProgressHighlight: DeviceLifecycleProgress?
  112. var cgmSensorExpiresAt: Date?
  113. var cgmWarmupEndsAt: Date?
  114. var listOfCGM: [CGMModel] = []
  115. var cgmCurrent = cgmDefaultModel
  116. var pumpInitialSettings = PumpConfig.PumpInitialSettings.default
  117. var shouldRunDeleteOnSettingsChange = true
  118. var showCarbsRequiredBadge: Bool = true
  119. private(set) var setupPumpType: PumpConfig.PumpType = .minimed
  120. var minForecast: [Int] = []
  121. var maxForecast: [Int] = []
  122. var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
  123. var forecastDisplayType: ForecastDisplayType = .cone
  124. var minYAxisValue: Decimal = 39
  125. var maxYAxisValue: Decimal = 200
  126. var minValueCobChart: Decimal = 0
  127. var maxValueCobChart: Decimal = 20
  128. var minValueIobChart: Decimal = 0
  129. var maxValueIobChart: Decimal = 5
  130. let taskContext = CoreDataStack.shared.newTaskContext()
  131. let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
  132. let carbsFetchContext = CoreDataStack.shared.newTaskContext()
  133. let fpuFetchContext = CoreDataStack.shared.newTaskContext()
  134. let determinationFetchContext = CoreDataStack.shared.newTaskContext()
  135. let tddFetchContext = CoreDataStack.shared.newTaskContext()
  136. let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
  137. let overrideFetchContext = CoreDataStack.shared.newTaskContext()
  138. let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
  139. let batteryFetchContext = CoreDataStack.shared.newTaskContext()
  140. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  141. // Queue for handling Core Data change notifications
  142. private let queue = DispatchQueue(label: "HomeStateModel.queue", qos: .userInitiated)
  143. private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
  144. private var subscriptions = Set<AnyCancellable>()
  145. typealias PumpEvent = PumpEventStored.EventType
  146. override init() {
  147. super.init()
  148. }
  149. override func subscribe() {
  150. coreDataPublisher =
  151. changedObjectsOnManagedObjectContextDidSavePublisher()
  152. .receive(on: queue)
  153. .share()
  154. .eraseToAnyPublisher()
  155. registerSubscribers()
  156. registerHandlers()
  157. // Parallelize Setup functions
  158. setupHomeViewConcurrently()
  159. }
  160. private func setupHomeViewConcurrently() {
  161. Task {
  162. // We need to initialize settings and observers first
  163. await self.setupSettings()
  164. await self.setupPumpSettings()
  165. await self.setupCGMSettings()
  166. self.registerObservers()
  167. // The rest can be initialized concurrently
  168. await withTaskGroup(of: Void.self) { group in
  169. group.addTask {
  170. self.setupGlucoseArray()
  171. }
  172. group.addTask {
  173. self.setupCarbsArray()
  174. }
  175. group.addTask {
  176. self.setupFPUsArray()
  177. }
  178. group.addTask {
  179. self.setupDeterminationsArray()
  180. }
  181. group.addTask {
  182. self.setupTDDArray()
  183. }
  184. group.addTask {
  185. self.setupInsulinArray()
  186. }
  187. group.addTask {
  188. self.setupLastBolus()
  189. }
  190. group.addTask {
  191. self.setupBatteryArray()
  192. }
  193. group.addTask {
  194. await self.setupBasalProfile()
  195. }
  196. group.addTask {
  197. await self.setupGlucoseTargets()
  198. }
  199. group.addTask {
  200. self.setupReservoir()
  201. }
  202. group.addTask {
  203. self.setupOverrides()
  204. }
  205. group.addTask {
  206. self.setupOverrideRunStored()
  207. }
  208. group.addTask {
  209. self.setupTempTargetsStored()
  210. }
  211. group.addTask {
  212. self.setupTempTargetsRunStored()
  213. }
  214. group.addTask {
  215. self.iobService.updateIOB()
  216. }
  217. }
  218. }
  219. }
  220. // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
  221. private func registerSubscribers() {
  222. iobService.iobPublisher
  223. .receive(on: DispatchQueue.main)
  224. .sink { [weak self] _ in
  225. guard let self = self else { return }
  226. self.currentIOB = self.iobService.currentIOB ?? 0
  227. }
  228. .store(in: &subscriptions)
  229. glucoseStorage.updatePublisher
  230. .receive(on: queue)
  231. .sink { [weak self] _ in
  232. guard let self = self else { return }
  233. self.setupGlucoseArray()
  234. }
  235. .store(in: &subscriptions)
  236. carbsStorage.updatePublisher
  237. .receive(on: queue)
  238. .sink { [weak self] _ in
  239. guard let self = self else { return }
  240. self.setupFPUsArray()
  241. }
  242. .store(in: &subscriptions)
  243. }
  244. private func registerHandlers() {
  245. coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
  246. guard let self = self else { return }
  247. self.setupDeterminationsArray()
  248. }.store(in: &subscriptions)
  249. coreDataPublisher?.filteredByEntityName("TDDStored").sink { [weak self] _ in
  250. guard let self = self else { return }
  251. self.setupTDDArray()
  252. }.store(in: &subscriptions)
  253. coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
  254. guard let self = self else { return }
  255. self.setupGlucoseArray()
  256. }.store(in: &subscriptions)
  257. coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
  258. guard let self = self else { return }
  259. self.setupCarbsArray()
  260. }.store(in: &subscriptions)
  261. coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
  262. guard let self = self else { return }
  263. self.setupInsulinArray()
  264. self.setupLastBolus()
  265. self.displayPumpStatusHighlightMessage()
  266. self.displayPumpStatusBadge()
  267. }.store(in: &subscriptions)
  268. coreDataPublisher?.filteredByEntityName("OpenAPS_Battery").sink { [weak self] _ in
  269. guard let self = self else { return }
  270. self.setupBatteryArray()
  271. }.store(in: &subscriptions)
  272. coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
  273. guard let self = self else { return }
  274. self.setupOverrides()
  275. }.store(in: &subscriptions)
  276. coreDataPublisher?.filteredByEntityName("OverrideRunStored").sink { [weak self] _ in
  277. guard let self = self else { return }
  278. self.setupOverrideRunStored()
  279. }.store(in: &subscriptions)
  280. coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
  281. guard let self = self else { return }
  282. self.setupTempTargetsStored()
  283. }.store(in: &subscriptions)
  284. coreDataPublisher?.filteredByEntityName("TempTargetRunStored").sink { [weak self] _ in
  285. guard let self = self else { return }
  286. self.setupTempTargetsRunStored()
  287. }.store(in: &subscriptions)
  288. }
  289. private func registerObservers() {
  290. broadcaster.register(DeterminationObserver.self, observer: self)
  291. broadcaster.register(SettingsObserver.self, observer: self)
  292. broadcaster.register(PreferencesObserver.self, observer: self)
  293. broadcaster.register(PumpSettingsObserver.self, observer: self)
  294. broadcaster.register(BasalProfileObserver.self, observer: self)
  295. broadcaster.register(BGTargetsObserver.self, observer: self)
  296. broadcaster.register(PumpReservoirObserver.self, observer: self)
  297. broadcaster.register(PumpDeactivatedObserver.self, observer: self)
  298. timer.eventHandler = {
  299. DispatchQueue.main.async { [weak self] in
  300. guard let self else { return }
  301. self.timerDate = Date()
  302. // The publisher only re-emits on state changes; re-pull
  303. // so the arc + countdowns + status text advance during
  304. // warmup / stabilizing / expiry. Simulator has no
  305. // CGMManager, so fall back to reading its synthetic
  306. // lifecycle / highlight so the bobble sees the same
  307. // data shape a real CGM would deliver.
  308. let manager = self.fetchGlucoseManager.cgmManager
  309. let source = self.fetchGlucoseManager.glucoseSource
  310. let progress: DeviceLifecycleProgress?
  311. let highlight: DeviceStatusHighlight?
  312. if let manager {
  313. progress = manager.cgmLifecycleProgress
  314. highlight = manager.cgmStatusHighlight
  315. } else if let sim = source as? GlucoseSimulatorSource {
  316. progress = sim.cgmLifecycleProgress
  317. highlight = sim.cgmStatusHighlight
  318. } else {
  319. progress = nil
  320. highlight = nil
  321. }
  322. self.cgmProgressHighlight = progress
  323. if let highlight {
  324. self.cgmDisplayState = CgmDisplayState(
  325. localizedMessage: highlight.localizedMessage,
  326. imageName: highlight.imageName,
  327. status: CgmDisplayStatus.from(highlight.state)
  328. )
  329. } else {
  330. self.cgmDisplayState = nil
  331. }
  332. self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
  333. manager: manager,
  334. glucoseSource: source,
  335. lifecycle: progress
  336. )
  337. self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(manager: manager)
  338. }
  339. }
  340. timer.resume()
  341. fetchGlucoseManager.cgmDisplayState
  342. .receive(on: DispatchQueue.main)
  343. .sink { [weak self] state in self?.cgmDisplayState = state }
  344. .store(in: &lifetime)
  345. fetchGlucoseManager.cgmProgressHighlight
  346. .receive(on: DispatchQueue.main)
  347. .sink { [weak self] progress in
  348. guard let self else { return }
  349. self.cgmProgressHighlight = progress
  350. self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
  351. manager: self.fetchGlucoseManager.cgmManager,
  352. glucoseSource: self.fetchGlucoseManager.glucoseSource,
  353. lifecycle: progress
  354. )
  355. self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(
  356. manager: self.fetchGlucoseManager.cgmManager
  357. )
  358. }
  359. .store(in: &lifetime)
  360. apsManager.isLooping
  361. .receive(on: DispatchQueue.main)
  362. .weakAssign(to: \.isLooping, on: self)
  363. .store(in: &lifetime)
  364. apsManager.lastLoopDateSubject
  365. .receive(on: DispatchQueue.main)
  366. .weakAssign(to: \.lastLoopDate, on: self)
  367. .store(in: &lifetime)
  368. apsManager.pumpName
  369. .receive(on: DispatchQueue.main)
  370. .weakAssign(to: \.pumpName, on: self)
  371. .store(in: &lifetime)
  372. apsManager.pumpExpiresAtDate
  373. .receive(on: DispatchQueue.main)
  374. .weakAssign(to: \.pumpExpiresAtDate, on: self)
  375. .store(in: &lifetime)
  376. apsManager.pumpActivatedAtDate
  377. .receive(on: DispatchQueue.main)
  378. .weakAssign(to: \.pumpActivatedAtDate, on: self)
  379. .store(in: &lifetime)
  380. apsManager.lastError
  381. .receive(on: DispatchQueue.main)
  382. .map { [weak self] error in
  383. self?.errorDate = error == nil ? nil : Date()
  384. if let error = error {
  385. info(.default, String(describing: error), notificationText: error.localizedDescription)
  386. }
  387. return error?.localizedDescription
  388. }
  389. .weakAssign(to: \.errorMessage, on: self)
  390. .store(in: &lifetime)
  391. apsManager.bolusProgress
  392. .receive(on: DispatchQueue.main)
  393. .weakAssign(to: \.bolusProgress, on: self)
  394. .store(in: &lifetime)
  395. apsManager.pumpDisplayState
  396. .receive(on: DispatchQueue.main)
  397. .sink { [weak self] state in
  398. guard let self = self else { return }
  399. self.pumpDisplayState = state
  400. if state == nil {
  401. self.reservoir = nil
  402. self.battery = nil
  403. self.pumpName = ""
  404. self.pumpExpiresAtDate = nil
  405. self.shouldDisplayPumpSetupSheet = false
  406. } else {
  407. self.setupReservoir()
  408. self.displayPumpStatusHighlightMessage()
  409. self.displayPumpStatusBadge()
  410. self.setupBatteryArray()
  411. }
  412. }
  413. .store(in: &lifetime)
  414. }
  415. private enum SettingType {
  416. case basal
  417. case carbRatio
  418. case bgTarget
  419. case isf
  420. }
  421. @MainActor private func setupSettings() async {
  422. units = settingsManager.settings.units
  423. allowManualTemp = !settingsManager.settings.closedLoop
  424. closedLoop = settingsManager.settings.closedLoop
  425. lastLoopDate = apsManager.lastLoopDate
  426. alarm = provider.glucoseStorage.alarm
  427. manualTempBasal = apsManager.isManualTempBasal
  428. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  429. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  430. autosensMax = settingsManager.preferences.autosensMax
  431. lowGlucose = settingsManager.settings.low
  432. highGlucose = settingsManager.settings.high
  433. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  434. displayXgridLines = settingsManager.settings.xGridLines
  435. displayYgridLines = settingsManager.settings.yGridLines
  436. bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
  437. thresholdLines = settingsManager.settings.rulerMarks
  438. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  439. forecastDisplayType = settingsManager.settings.forecastDisplayType
  440. isExerciseModeActive = settingsManager.preferences.exerciseMode
  441. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  442. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  443. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  444. maxIOB = settingsManager.preferences.maxIOB
  445. }
  446. @MainActor private func setupCGMSettings() async {
  447. cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
  448. listOfCGM = (
  449. CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  450. CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  451. } +
  452. pluginCGMManager.availableCGMManagers.map {
  453. CGMModel(
  454. id: $0.identifier,
  455. type: CGMType.plugin,
  456. displayName: $0.localizedTitle,
  457. subtitle: $0.localizedTitle
  458. )
  459. }
  460. ).sorted(by: { lhs, rhs in
  461. if lhs.displayName == "None" {
  462. return true
  463. } else if rhs.displayName == "None" {
  464. return false
  465. } else {
  466. return lhs.displayName < rhs.displayName
  467. }
  468. })
  469. switch settingsManager.settings.cgm {
  470. case .plugin:
  471. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  472. cgmCurrent = CGMModel(
  473. id: settingsManager.settings.cgmPluginIdentifier,
  474. type: .plugin,
  475. displayName: cgmPluginInfo.displayName,
  476. subtitle: cgmPluginInfo.subtitle
  477. )
  478. } else {
  479. // no more type of plugin available - fallback to default
  480. cgmCurrent = cgmDefaultModel
  481. }
  482. default:
  483. cgmCurrent = CGMModel(
  484. id: settingsManager.settings.cgm.id,
  485. type: settingsManager.settings.cgm,
  486. displayName: settingsManager.settings.cgm.displayName,
  487. subtitle: settingsManager.settings.cgm.subtitle
  488. )
  489. }
  490. }
  491. func addPump(_ type: PumpConfig.PumpType) {
  492. setupPumpType = type
  493. shouldDisplayPumpSetupSheet = true
  494. }
  495. func addCGM(cgm: CGMModel) {
  496. cgmCurrent = cgm
  497. switch cgmCurrent.type {
  498. case .plugin:
  499. shouldDisplayCGMSetupSheet = true
  500. default:
  501. shouldDisplayCGMSetupSheet = true
  502. settingsManager.settings.cgm = cgmCurrent.type
  503. settingsManager.settings.cgmPluginIdentifier = ""
  504. fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  505. broadcaster.notify(GlucoseObserver.self, on: .main) {
  506. $0.glucoseDidUpdate([])
  507. }
  508. }
  509. }
  510. func deleteCGM() {
  511. fetchGlucoseManager.performOnCGMManagerQueue {
  512. // Call plugin functionality on the manager queue (or at least attempt to)
  513. Task {
  514. await self.fetchGlucoseManager?.deleteGlucoseSource()
  515. // UI updates go back to Main
  516. await MainActor.run {
  517. self.shouldDisplayCGMSetupSheet = false
  518. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  519. $0.glucoseDidUpdate([])
  520. }
  521. }
  522. }
  523. }
  524. }
  525. /// Display the eventual status message provided by the manager of the pump
  526. /// Only display if state is warning or critical message else return nil
  527. private func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
  528. DispatchQueue.main.async { [weak self] in
  529. guard let self = self else { return }
  530. if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
  531. statusHighlight.state == .warning || statusHighlight.state == .critical, !didDeactivate
  532. {
  533. pumpStatusHighlightMessage = (statusHighlight.state == .warning ? "⚠️\n" : "‼️\n") + statusHighlight
  534. .localizedMessage
  535. } else {
  536. pumpStatusHighlightMessage = nil
  537. }
  538. }
  539. }
  540. private func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
  541. DispatchQueue.main.async { [weak self] in
  542. guard let self = self else { return }
  543. if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,
  544. let image = statusBadge.image, !didDeactivate
  545. {
  546. pumpStatusBadgeImage = image
  547. pumpStatusBadgeColor = statusBadge.state == .critical ? .critical : .warning
  548. } else {
  549. pumpStatusBadgeImage = nil
  550. pumpStatusBadgeColor = nil
  551. }
  552. }
  553. }
  554. func runLoop() {
  555. provider.heartbeatNow()
  556. }
  557. func showProgressView() {
  558. glucoseStorage
  559. .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
  560. }
  561. func cancelBolus() {
  562. Task {
  563. await apsManager.cancelBolus(nil)
  564. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  565. try await apsManager.determineBasalSync()
  566. }
  567. }
  568. private func setupPumpSettings() async {
  569. let settings = await provider.pumpSettings()
  570. await MainActor.run {
  571. self.maxBasal = settings.maxBasal
  572. self.pumpInitialSettings.maxBasalRateUnitsPerHour = Double(settings.maxBasal)
  573. self.pumpInitialSettings.maxBolusUnits = Double(settings.maxBolus)
  574. }
  575. }
  576. private func setupBasalProfile() async {
  577. let basalProfile = await provider.getBasalProfile()
  578. await MainActor.run {
  579. self.basalProfile = basalProfile
  580. if let schedule = BasalRateSchedule(
  581. dailyItems: basalProfile
  582. .map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) }
  583. ) {
  584. self.pumpInitialSettings.basalSchedule = schedule
  585. }
  586. }
  587. }
  588. private func setupGlucoseTargets() async {
  589. let bgTargets = await provider.getBGTargets()
  590. let targetProfiles = processFetchedTargets(bgTargets, startMarker: startMarker)
  591. await MainActor.run {
  592. self.bgTargets = bgTargets
  593. self.targetProfiles = targetProfiles
  594. }
  595. }
  596. private func setupReservoir() {
  597. Task {
  598. let reservoir = await provider.pumpReservoir()
  599. await MainActor.run {
  600. self.reservoir = reservoir
  601. }
  602. }
  603. }
  604. private func getCurrentGlucoseTarget() async {
  605. let now = Date()
  606. let calendar = Calendar.current
  607. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  608. for (index, entry) in entries.enumerated() {
  609. guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
  610. debug(.default, "Invalid entry start time: \(entry.start)")
  611. continue
  612. }
  613. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  614. let entryStartTime = calendar.date(
  615. bySettingHour: entryComponents.hour!,
  616. minute: entryComponents.minute!,
  617. second: entryComponents.second!,
  618. of: now
  619. )!
  620. let entryEndTime: Date
  621. if index < entries.count - 1,
  622. let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
  623. {
  624. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  625. entryEndTime = calendar.date(
  626. bySettingHour: nextEntryComponents.hour!,
  627. minute: nextEntryComponents.minute!,
  628. second: nextEntryComponents.second!,
  629. of: now
  630. )!
  631. } else {
  632. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  633. }
  634. if now >= entryStartTime, now < entryEndTime {
  635. await MainActor.run {
  636. currentGlucoseTarget = entry.value
  637. }
  638. return
  639. }
  640. }
  641. }
  642. /// Sensor expiration for the home label. Prefers manager-reported
  643. /// dates; reverse-derives from `lifecycle.percentComplete` when not.
  644. /// `activatedAt` must be session start, not transmitter activation.
  645. private static func resolveSensorExpiresAt(
  646. manager: CGMManagerUI?,
  647. glucoseSource: GlucoseSource?,
  648. lifecycle: DeviceLifecycleProgress?
  649. ) -> Date? {
  650. if let sim = glucoseSource as? GlucoseSimulatorSource {
  651. return sim.simulatedSensorExpiresAt
  652. }
  653. guard let manager else { return nil }
  654. // Once a G7 enters grace period, `sensorExpiresAt` is in the past
  655. // and would collapse the bobble countdown to "<1m" while the arc
  656. // (driven by lifecycle.percentComplete against `sensorEndsAt`) is
  657. // still mid-progress. Fall back to `sensorEndsAt` so bobble and
  658. // arc agree, and the user sees grace-period time remaining.
  659. if let g7 = manager as? G7CGMManager {
  660. let now = Date()
  661. if let exp = g7.sensorExpiresAt, exp > now { return exp }
  662. return g7.sensorEndsAt ?? g7.sensorExpiresAt
  663. }
  664. if let g6 = manager as? G6CGMManager, let exp = g6.latestReading?.sessionExpDate { return exp }
  665. if let g5 = manager as? G5CGMManager, let exp = g5.latestReading?.sessionExpDate { return exp }
  666. let activatedAt: Date?
  667. if let g7 = manager as? G7CGMManager {
  668. activatedAt = g7.sensorActivatedAt
  669. } else if let libre = manager as? LibreTransmitterManagerV3 {
  670. activatedAt = libre.sensorInfoObservable.activatedAt
  671. } else {
  672. activatedAt = nil
  673. }
  674. guard let activatedAt,
  675. let lifecycle,
  676. lifecycle.percentComplete > 0.001
  677. else { return nil }
  678. let elapsed = Date().timeIntervalSince(activatedAt)
  679. guard elapsed > 0 else { return nil }
  680. return activatedAt.addingTimeInterval(elapsed / lifecycle.percentComplete)
  681. }
  682. /// Wall-clock end of the sensor's warmup window; `nil` when not warming up.
  683. private static func resolveWarmupEndsAt(manager: CGMManagerUI?) -> Date? {
  684. guard let manager else { return nil }
  685. if let g7 = manager as? G7CGMManager {
  686. guard let ends = g7.sensorFinishesWarmupAt, ends > Date() else { return nil }
  687. return ends
  688. }
  689. if let g6 = manager as? G6CGMManager, let start = g6.latestReading?.sessionStartDate {
  690. let window: TimeInterval = g6.isAnubis ? 50 * 60 : 2 * 60 * 60
  691. let ends = start.addingTimeInterval(window)
  692. return ends > Date() ? ends : nil
  693. }
  694. if let g5 = manager as? G5CGMManager, let start = g5.latestReading?.sessionStartDate {
  695. let ends = start.addingTimeInterval(2 * 60 * 60)
  696. return ends > Date() ? ends : nil
  697. }
  698. return nil
  699. }
  700. }
  701. }
  702. extension Home.StateModel:
  703. DeterminationObserver,
  704. SettingsObserver,
  705. PreferencesObserver,
  706. PumpSettingsObserver,
  707. BasalProfileObserver,
  708. BGTargetsObserver,
  709. PumpReservoirObserver,
  710. PumpDeactivatedObserver
  711. {
  712. func determinationDidUpdate(_: Determination) {
  713. waitForSuggestion = false
  714. }
  715. func settingsDidChange(_ settings: TrioSettings) {
  716. allowManualTemp = !settings.closedLoop
  717. closedLoop = settingsManager.settings.closedLoop
  718. units = settingsManager.settings.units
  719. manualTempBasal = apsManager.isManualTempBasal
  720. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  721. lowGlucose = settingsManager.settings.low
  722. highGlucose = settingsManager.settings.high
  723. Task {
  724. await getCurrentGlucoseTarget()
  725. await setupGlucoseTargets()
  726. }
  727. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  728. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  729. displayXgridLines = settingsManager.settings.xGridLines
  730. displayYgridLines = settingsManager.settings.yGridLines
  731. thresholdLines = settingsManager.settings.rulerMarks
  732. bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
  733. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  734. forecastDisplayType = settingsManager.settings.forecastDisplayType
  735. cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
  736. displayPumpStatusHighlightMessage()
  737. displayPumpStatusBadge()
  738. setupBatteryArray()
  739. Task {
  740. await setupCGMSettings()
  741. }
  742. if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
  743. shouldRunDeleteOnSettingsChange = false
  744. cgmCurrent = cgmDefaultModel
  745. DispatchQueue.main.async {
  746. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  747. $0.glucoseDidUpdate([])
  748. }
  749. }
  750. } else {
  751. shouldRunDeleteOnSettingsChange = true
  752. }
  753. }
  754. func preferencesDidChange(_: Preferences) {
  755. autosensMax = settingsManager.preferences.autosensMax
  756. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  757. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  758. isExerciseModeActive = settingsManager.preferences.exerciseMode
  759. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  760. maxIOB = settingsManager.preferences.maxIOB
  761. }
  762. func pumpSettingsDidChange(_: PumpSettings) {
  763. Task {
  764. await setupPumpSettings()
  765. setupBatteryArray()
  766. }
  767. }
  768. func basalProfileDidChange(_: [BasalProfileEntry]) {
  769. Task {
  770. await setupBasalProfile()
  771. }
  772. }
  773. func bgTargetsDidChange(_: BGTargets) {
  774. Task {
  775. await setupGlucoseTargets()
  776. }
  777. }
  778. func pumpReservoirDidChange(_: Decimal) {
  779. setupReservoir()
  780. displayPumpStatusHighlightMessage()
  781. displayPumpStatusBadge()
  782. }
  783. func pumpDeactivatedDidChange() {
  784. displayPumpStatusHighlightMessage(true)
  785. displayPumpStatusBadge(true)
  786. batteryFromPersistence = []
  787. }
  788. }
  789. extension Home.StateModel: PumpManagerOnboardingDelegate {
  790. func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
  791. provider.apsManager.pumpManager = pumpManager
  792. if let insulinType = pumpManager.status.insulinType {
  793. settingsManager.updateInsulinCurve(insulinType)
  794. }
  795. }
  796. func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
  797. // nothing to do
  798. }
  799. func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
  800. // nothing to do
  801. }
  802. }