HomeStateModel.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  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.
  305. let manager = self.fetchGlucoseManager.cgmManager
  306. let progress = manager?.cgmLifecycleProgress
  307. self.cgmProgressHighlight = progress
  308. if let highlight = manager?.cgmStatusHighlight {
  309. self.cgmDisplayState = CgmDisplayState(
  310. localizedMessage: highlight.localizedMessage,
  311. status: CgmDisplayStatus.from(highlight.state)
  312. )
  313. } else {
  314. self.cgmDisplayState = nil
  315. }
  316. self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
  317. manager: manager,
  318. glucoseSource: self.fetchGlucoseManager.glucoseSource,
  319. lifecycle: progress
  320. )
  321. self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(manager: manager)
  322. }
  323. }
  324. timer.resume()
  325. fetchGlucoseManager.cgmDisplayState
  326. .receive(on: DispatchQueue.main)
  327. .sink { [weak self] state in self?.cgmDisplayState = state }
  328. .store(in: &lifetime)
  329. fetchGlucoseManager.cgmProgressHighlight
  330. .receive(on: DispatchQueue.main)
  331. .sink { [weak self] progress in
  332. guard let self else { return }
  333. self.cgmProgressHighlight = progress
  334. self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
  335. manager: self.fetchGlucoseManager.cgmManager,
  336. glucoseSource: self.fetchGlucoseManager.glucoseSource,
  337. lifecycle: progress
  338. )
  339. self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(
  340. manager: self.fetchGlucoseManager.cgmManager
  341. )
  342. }
  343. .store(in: &lifetime)
  344. apsManager.isLooping
  345. .receive(on: DispatchQueue.main)
  346. .weakAssign(to: \.isLooping, on: self)
  347. .store(in: &lifetime)
  348. apsManager.lastLoopDateSubject
  349. .receive(on: DispatchQueue.main)
  350. .weakAssign(to: \.lastLoopDate, on: self)
  351. .store(in: &lifetime)
  352. apsManager.pumpName
  353. .receive(on: DispatchQueue.main)
  354. .weakAssign(to: \.pumpName, on: self)
  355. .store(in: &lifetime)
  356. apsManager.pumpExpiresAtDate
  357. .receive(on: DispatchQueue.main)
  358. .weakAssign(to: \.pumpExpiresAtDate, on: self)
  359. .store(in: &lifetime)
  360. apsManager.pumpActivatedAtDate
  361. .receive(on: DispatchQueue.main)
  362. .weakAssign(to: \.pumpActivatedAtDate, on: self)
  363. .store(in: &lifetime)
  364. apsManager.lastError
  365. .receive(on: DispatchQueue.main)
  366. .map { [weak self] error in
  367. self?.errorDate = error == nil ? nil : Date()
  368. if let error = error {
  369. info(.default, String(describing: error), notificationText: error.localizedDescription)
  370. }
  371. return error?.localizedDescription
  372. }
  373. .weakAssign(to: \.errorMessage, on: self)
  374. .store(in: &lifetime)
  375. apsManager.bolusProgress
  376. .receive(on: DispatchQueue.main)
  377. .weakAssign(to: \.bolusProgress, on: self)
  378. .store(in: &lifetime)
  379. apsManager.pumpDisplayState
  380. .receive(on: DispatchQueue.main)
  381. .sink { [weak self] state in
  382. guard let self = self else { return }
  383. self.pumpDisplayState = state
  384. if state == nil {
  385. self.reservoir = nil
  386. self.battery = nil
  387. self.pumpName = ""
  388. self.pumpExpiresAtDate = nil
  389. self.shouldDisplayPumpSetupSheet = false
  390. } else {
  391. self.setupReservoir()
  392. self.displayPumpStatusHighlightMessage()
  393. self.displayPumpStatusBadge()
  394. self.setupBatteryArray()
  395. }
  396. }
  397. .store(in: &lifetime)
  398. }
  399. private enum SettingType {
  400. case basal
  401. case carbRatio
  402. case bgTarget
  403. case isf
  404. }
  405. @MainActor private func setupSettings() async {
  406. units = settingsManager.settings.units
  407. allowManualTemp = !settingsManager.settings.closedLoop
  408. closedLoop = settingsManager.settings.closedLoop
  409. lastLoopDate = apsManager.lastLoopDate
  410. alarm = provider.glucoseStorage.alarm
  411. manualTempBasal = apsManager.isManualTempBasal
  412. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  413. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  414. autosensMax = settingsManager.preferences.autosensMax
  415. lowGlucose = settingsManager.settings.low
  416. highGlucose = settingsManager.settings.high
  417. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  418. displayXgridLines = settingsManager.settings.xGridLines
  419. displayYgridLines = settingsManager.settings.yGridLines
  420. bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
  421. thresholdLines = settingsManager.settings.rulerMarks
  422. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  423. forecastDisplayType = settingsManager.settings.forecastDisplayType
  424. isExerciseModeActive = settingsManager.preferences.exerciseMode
  425. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  426. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  427. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  428. maxIOB = settingsManager.preferences.maxIOB
  429. }
  430. @MainActor private func setupCGMSettings() async {
  431. cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
  432. listOfCGM = (
  433. CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  434. CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  435. } +
  436. pluginCGMManager.availableCGMManagers.map {
  437. CGMModel(
  438. id: $0.identifier,
  439. type: CGMType.plugin,
  440. displayName: $0.localizedTitle,
  441. subtitle: $0.localizedTitle
  442. )
  443. }
  444. ).sorted(by: { lhs, rhs in
  445. if lhs.displayName == "None" {
  446. return true
  447. } else if rhs.displayName == "None" {
  448. return false
  449. } else {
  450. return lhs.displayName < rhs.displayName
  451. }
  452. })
  453. switch settingsManager.settings.cgm {
  454. case .plugin:
  455. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  456. cgmCurrent = CGMModel(
  457. id: settingsManager.settings.cgmPluginIdentifier,
  458. type: .plugin,
  459. displayName: cgmPluginInfo.displayName,
  460. subtitle: cgmPluginInfo.subtitle
  461. )
  462. } else {
  463. // no more type of plugin available - fallback to default
  464. cgmCurrent = cgmDefaultModel
  465. }
  466. default:
  467. cgmCurrent = CGMModel(
  468. id: settingsManager.settings.cgm.id,
  469. type: settingsManager.settings.cgm,
  470. displayName: settingsManager.settings.cgm.displayName,
  471. subtitle: settingsManager.settings.cgm.subtitle
  472. )
  473. }
  474. }
  475. func addPump(_ type: PumpConfig.PumpType) {
  476. setupPumpType = type
  477. shouldDisplayPumpSetupSheet = true
  478. }
  479. func addCGM(cgm: CGMModel) {
  480. cgmCurrent = cgm
  481. switch cgmCurrent.type {
  482. case .plugin:
  483. shouldDisplayCGMSetupSheet = true
  484. default:
  485. shouldDisplayCGMSetupSheet = true
  486. settingsManager.settings.cgm = cgmCurrent.type
  487. settingsManager.settings.cgmPluginIdentifier = ""
  488. fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  489. broadcaster.notify(GlucoseObserver.self, on: .main) {
  490. $0.glucoseDidUpdate([])
  491. }
  492. }
  493. }
  494. func deleteCGM() {
  495. fetchGlucoseManager.performOnCGMManagerQueue {
  496. // Call plugin functionality on the manager queue (or at least attempt to)
  497. Task {
  498. await self.fetchGlucoseManager?.deleteGlucoseSource()
  499. // UI updates go back to Main
  500. await MainActor.run {
  501. self.shouldDisplayCGMSetupSheet = false
  502. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  503. $0.glucoseDidUpdate([])
  504. }
  505. }
  506. }
  507. }
  508. }
  509. /// Display the eventual status message provided by the manager of the pump
  510. /// Only display if state is warning or critical message else return nil
  511. private func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
  512. DispatchQueue.main.async { [weak self] in
  513. guard let self = self else { return }
  514. if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
  515. statusHighlight.state == .warning || statusHighlight.state == .critical, !didDeactivate
  516. {
  517. pumpStatusHighlightMessage = (statusHighlight.state == .warning ? "⚠️\n" : "‼️\n") + statusHighlight
  518. .localizedMessage
  519. } else {
  520. pumpStatusHighlightMessage = nil
  521. }
  522. }
  523. }
  524. private func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
  525. DispatchQueue.main.async { [weak self] in
  526. guard let self = self else { return }
  527. if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,
  528. let image = statusBadge.image, !didDeactivate
  529. {
  530. pumpStatusBadgeImage = image
  531. pumpStatusBadgeColor = statusBadge.state == .critical ? .critical : .warning
  532. } else {
  533. pumpStatusBadgeImage = nil
  534. pumpStatusBadgeColor = nil
  535. }
  536. }
  537. }
  538. func runLoop() {
  539. provider.heartbeatNow()
  540. }
  541. func showProgressView() {
  542. glucoseStorage
  543. .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
  544. }
  545. func cancelBolus() {
  546. Task {
  547. await apsManager.cancelBolus(nil)
  548. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  549. try await apsManager.determineBasalSync()
  550. }
  551. }
  552. private func setupPumpSettings() async {
  553. let settings = await provider.pumpSettings()
  554. await MainActor.run {
  555. self.maxBasal = settings.maxBasal
  556. self.pumpInitialSettings.maxBasalRateUnitsPerHour = Double(settings.maxBasal)
  557. self.pumpInitialSettings.maxBolusUnits = Double(settings.maxBolus)
  558. }
  559. }
  560. private func setupBasalProfile() async {
  561. let basalProfile = await provider.getBasalProfile()
  562. await MainActor.run {
  563. self.basalProfile = basalProfile
  564. if let schedule = BasalRateSchedule(
  565. dailyItems: basalProfile
  566. .map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) }
  567. ) {
  568. self.pumpInitialSettings.basalSchedule = schedule
  569. }
  570. }
  571. }
  572. private func setupGlucoseTargets() async {
  573. let bgTargets = await provider.getBGTargets()
  574. let targetProfiles = processFetchedTargets(bgTargets, startMarker: startMarker)
  575. await MainActor.run {
  576. self.bgTargets = bgTargets
  577. self.targetProfiles = targetProfiles
  578. }
  579. }
  580. private func setupReservoir() {
  581. Task {
  582. let reservoir = await provider.pumpReservoir()
  583. await MainActor.run {
  584. self.reservoir = reservoir
  585. }
  586. }
  587. }
  588. private func getCurrentGlucoseTarget() async {
  589. let now = Date()
  590. let calendar = Calendar.current
  591. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  592. for (index, entry) in entries.enumerated() {
  593. guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
  594. debug(.default, "Invalid entry start time: \(entry.start)")
  595. continue
  596. }
  597. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  598. let entryStartTime = calendar.date(
  599. bySettingHour: entryComponents.hour!,
  600. minute: entryComponents.minute!,
  601. second: entryComponents.second!,
  602. of: now
  603. )!
  604. let entryEndTime: Date
  605. if index < entries.count - 1,
  606. let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
  607. {
  608. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  609. entryEndTime = calendar.date(
  610. bySettingHour: nextEntryComponents.hour!,
  611. minute: nextEntryComponents.minute!,
  612. second: nextEntryComponents.second!,
  613. of: now
  614. )!
  615. } else {
  616. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  617. }
  618. if now >= entryStartTime, now < entryEndTime {
  619. await MainActor.run {
  620. currentGlucoseTarget = entry.value
  621. }
  622. return
  623. }
  624. }
  625. }
  626. /// Sensor expiration for the home label. Prefers manager-reported
  627. /// dates; reverse-derives from `lifecycle.percentComplete` when not.
  628. /// `activatedAt` must be session start, not transmitter activation.
  629. private static func resolveSensorExpiresAt(
  630. manager: CGMManagerUI?,
  631. glucoseSource: GlucoseSource?,
  632. lifecycle: DeviceLifecycleProgress?
  633. ) -> Date? {
  634. if let sim = glucoseSource as? GlucoseSimulatorSource {
  635. return sim.simulatedSensorExpiresAt
  636. }
  637. guard let manager else { return nil }
  638. if let g7 = manager as? G7CGMManager, let exp = g7.sensorExpiresAt { return exp }
  639. if let g6 = manager as? G6CGMManager, let exp = g6.latestReading?.sessionExpDate { return exp }
  640. if let g5 = manager as? G5CGMManager, let exp = g5.latestReading?.sessionExpDate { return exp }
  641. let activatedAt: Date?
  642. if let g7 = manager as? G7CGMManager {
  643. activatedAt = g7.sensorActivatedAt
  644. } else if let libre = manager as? LibreTransmitterManagerV3 {
  645. activatedAt = libre.sensorInfoObservable.activatedAt
  646. } else {
  647. activatedAt = nil
  648. }
  649. guard let activatedAt,
  650. let lifecycle,
  651. lifecycle.percentComplete > 0.001
  652. else { return nil }
  653. let elapsed = Date().timeIntervalSince(activatedAt)
  654. guard elapsed > 0 else { return nil }
  655. return activatedAt.addingTimeInterval(elapsed / lifecycle.percentComplete)
  656. }
  657. /// Wall-clock end of the sensor's warmup window; `nil` when not warming up.
  658. private static func resolveWarmupEndsAt(manager: CGMManagerUI?) -> Date? {
  659. guard let manager else { return nil }
  660. if let g7 = manager as? G7CGMManager {
  661. guard let ends = g7.sensorFinishesWarmupAt, ends > Date() else { return nil }
  662. return ends
  663. }
  664. if let g6 = manager as? G6CGMManager, let start = g6.latestReading?.sessionStartDate {
  665. let window: TimeInterval = g6.isAnubis ? 50 * 60 : 2 * 60 * 60
  666. let ends = start.addingTimeInterval(window)
  667. return ends > Date() ? ends : nil
  668. }
  669. if let g5 = manager as? G5CGMManager, let start = g5.latestReading?.sessionStartDate {
  670. let ends = start.addingTimeInterval(2 * 60 * 60)
  671. return ends > Date() ? ends : nil
  672. }
  673. return nil
  674. }
  675. }
  676. }
  677. extension Home.StateModel:
  678. DeterminationObserver,
  679. SettingsObserver,
  680. PreferencesObserver,
  681. PumpSettingsObserver,
  682. BasalProfileObserver,
  683. BGTargetsObserver,
  684. PumpReservoirObserver,
  685. PumpDeactivatedObserver
  686. {
  687. func determinationDidUpdate(_: Determination) {
  688. waitForSuggestion = false
  689. }
  690. func settingsDidChange(_ settings: TrioSettings) {
  691. allowManualTemp = !settings.closedLoop
  692. closedLoop = settingsManager.settings.closedLoop
  693. units = settingsManager.settings.units
  694. manualTempBasal = apsManager.isManualTempBasal
  695. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  696. lowGlucose = settingsManager.settings.low
  697. highGlucose = settingsManager.settings.high
  698. Task {
  699. await getCurrentGlucoseTarget()
  700. await setupGlucoseTargets()
  701. }
  702. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  703. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  704. displayXgridLines = settingsManager.settings.xGridLines
  705. displayYgridLines = settingsManager.settings.yGridLines
  706. thresholdLines = settingsManager.settings.rulerMarks
  707. bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
  708. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  709. forecastDisplayType = settingsManager.settings.forecastDisplayType
  710. cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
  711. displayPumpStatusHighlightMessage()
  712. displayPumpStatusBadge()
  713. setupBatteryArray()
  714. Task {
  715. await setupCGMSettings()
  716. }
  717. if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
  718. shouldRunDeleteOnSettingsChange = false
  719. cgmCurrent = cgmDefaultModel
  720. DispatchQueue.main.async {
  721. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  722. $0.glucoseDidUpdate([])
  723. }
  724. }
  725. } else {
  726. shouldRunDeleteOnSettingsChange = true
  727. }
  728. }
  729. func preferencesDidChange(_: Preferences) {
  730. autosensMax = settingsManager.preferences.autosensMax
  731. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  732. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  733. isExerciseModeActive = settingsManager.preferences.exerciseMode
  734. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  735. maxIOB = settingsManager.preferences.maxIOB
  736. }
  737. func pumpSettingsDidChange(_: PumpSettings) {
  738. Task {
  739. await setupPumpSettings()
  740. setupBatteryArray()
  741. }
  742. }
  743. func basalProfileDidChange(_: [BasalProfileEntry]) {
  744. Task {
  745. await setupBasalProfile()
  746. }
  747. }
  748. func bgTargetsDidChange(_: BGTargets) {
  749. Task {
  750. await setupGlucoseTargets()
  751. }
  752. }
  753. func pumpReservoirDidChange(_: Decimal) {
  754. setupReservoir()
  755. displayPumpStatusHighlightMessage()
  756. displayPumpStatusBadge()
  757. }
  758. func pumpDeactivatedDidChange() {
  759. displayPumpStatusHighlightMessage(true)
  760. displayPumpStatusBadge(true)
  761. batteryFromPersistence = []
  762. }
  763. }
  764. extension Home.StateModel: PumpManagerOnboardingDelegate {
  765. func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
  766. provider.apsManager.pumpManager = pumpManager
  767. if let insulinType = pumpManager.status.insulinType {
  768. settingsManager.updateInsulinCurve(insulinType)
  769. }
  770. }
  771. func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
  772. // nothing to do
  773. }
  774. func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
  775. // nothing to do
  776. }
  777. }