HomeStateModel.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. import CGMBLEKitUI
  2. import Combine
  3. import CoreData
  4. import Foundation
  5. import LoopKit
  6. import LoopKitUI
  7. import Observation
  8. import SwiftDate
  9. import SwiftUI
  10. extension Home {
  11. @Observable final class StateModel: BaseStateModel<Provider> {
  12. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  13. @ObservationIgnored @Injected() var apsManager: APSManager!
  14. @ObservationIgnored @Injected() var pluginCGMManager: PluginManager!
  15. @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
  16. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  17. @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
  18. @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
  19. @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
  20. @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
  21. @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
  22. @ObservationIgnored @Injected() var bluetoothManager: BluetoothStateManager!
  23. @ObservationIgnored @Injected() var iobService: IOBService!
  24. var cgmStateModel: CGMSettings.StateModel {
  25. CGMSettings.StateModel.shared
  26. }
  27. private let timer = DispatchTimer(timeInterval: 5)
  28. private(set) var filteredHours = 24
  29. var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
  30. var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
  31. var manualGlucose: [BloodGlucose] = []
  32. var uploadStats = false
  33. var recentGlucose: BloodGlucose?
  34. var maxBasal: Decimal = 2
  35. var basalProfile: [BasalProfileEntry] = []
  36. var bgTargets = BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
  37. ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
  38. var targetProfiles: [TargetProfile] = []
  39. var timerDate = Date()
  40. var closedLoop = false
  41. var isLooping = false
  42. var statusTitle = ""
  43. var lastLoopDate: Date = .distantPast
  44. var battery: Battery?
  45. var reservoir: Decimal?
  46. var pumpName = ""
  47. var pumpExpiresAtDate: Date?
  48. var highTTraisesSens: Bool = false
  49. var lowTTlowersSens: Bool = false
  50. var isExerciseModeActive: Bool = false
  51. var settingHalfBasalTarget: Decimal = 160
  52. var percentage: Int = 100
  53. var shouldDisplayPumpSetupSheet = false
  54. var shouldDisplayCGMSetupSheet = false
  55. var errorMessage: String?
  56. var errorDate: Date?
  57. var bolusProgress: Decimal?
  58. var eventualBG: Int?
  59. var allowManualTemp = false
  60. var units: GlucoseUnits = .mgdL
  61. var pumpDisplayState: PumpDisplayState?
  62. var alarm: GlucoseAlarm?
  63. var manualTempBasal = false
  64. var isSmoothingEnabled = false
  65. var maxIOB: Decimal = 0.0
  66. var currentIOB: Decimal = 0.0
  67. var autosensMax: Decimal = 1.2
  68. var lowGlucose: Decimal = 70
  69. var highGlucose: Decimal = 180
  70. var currentGlucoseTarget: Decimal = 100
  71. var glucoseColorScheme: GlucoseColorScheme = .staticColor
  72. var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
  73. var displayXgridLines: Bool = false
  74. var displayYgridLines: Bool = false
  75. var thresholdLines: Bool = false
  76. var hours: Int16 = 6
  77. var totalBolus: Decimal = 0
  78. var isLoopStatusPresented: Bool = false
  79. var isLegendPresented: Bool = false
  80. var roundedTotalBolus: String = ""
  81. var selectedTab: Int = 0
  82. var waitForSuggestion: Bool = false
  83. var glucoseFromPersistence: [GlucoseStored] = []
  84. var latestTwoGlucoseValues: [GlucoseStored] = []
  85. var carbsFromPersistence: [CarbEntryStored] = []
  86. var fpusFromPersistence: [CarbEntryStored] = []
  87. var determinationsFromPersistence: [OrefDetermination] = []
  88. var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
  89. var fetchedTDDs: [TDD] = []
  90. var insulinFromPersistence: [PumpEventStored] = []
  91. var tempBasals: [PumpEventStored] = []
  92. var suspendAndResumeEvents: [PumpEventStored] = []
  93. var batteryFromPersistence: [OpenAPS_Battery] = []
  94. var lastPumpBolus: PumpEventStored?
  95. var overrides: [OverrideStored] = []
  96. var overrideRunStored: [OverrideRunStored] = []
  97. var tempTargetStored: [TempTargetStored] = []
  98. var tempTargetRunStored: [TempTargetRunStored] = []
  99. var isOverrideCancelled: Bool = false
  100. var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
  101. var pumpStatusHighlightMessage: String?
  102. var pumpStatusBadgeImage: UIImage?
  103. var pumpStatusBadgeColor: Color?
  104. var cgmAvailable: Bool = false
  105. var listOfCGM: [CGMModel] = []
  106. var cgmCurrent = cgmDefaultModel
  107. var pumpInitialSettings = PumpConfig.PumpInitialSettings.default
  108. var shouldRunDeleteOnSettingsChange = true
  109. var showCarbsRequiredBadge: Bool = true
  110. private(set) var setupPumpType: PumpConfig.PumpType = .minimed
  111. var minForecast: [Int] = []
  112. var maxForecast: [Int] = []
  113. var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
  114. var forecastDisplayType: ForecastDisplayType = .cone
  115. var minYAxisValue: Decimal = 39
  116. var maxYAxisValue: Decimal = 200
  117. var minValueCobChart: Decimal = 0
  118. var maxValueCobChart: Decimal = 20
  119. var minValueIobChart: Decimal = 0
  120. var maxValueIobChart: Decimal = 5
  121. let taskContext = CoreDataStack.shared.newTaskContext()
  122. let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
  123. let carbsFetchContext = CoreDataStack.shared.newTaskContext()
  124. let fpuFetchContext = CoreDataStack.shared.newTaskContext()
  125. let determinationFetchContext = CoreDataStack.shared.newTaskContext()
  126. let tddFetchContext = CoreDataStack.shared.newTaskContext()
  127. let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
  128. let overrideFetchContext = CoreDataStack.shared.newTaskContext()
  129. let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
  130. let batteryFetchContext = CoreDataStack.shared.newTaskContext()
  131. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  132. // Queue for handling Core Data change notifications
  133. private let queue = DispatchQueue(label: "HomeStateModel.queue", qos: .userInitiated)
  134. private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
  135. private var subscriptions = Set<AnyCancellable>()
  136. typealias PumpEvent = PumpEventStored.EventType
  137. override init() {
  138. super.init()
  139. }
  140. override func subscribe() {
  141. coreDataPublisher =
  142. changedObjectsOnManagedObjectContextDidSavePublisher()
  143. .receive(on: queue)
  144. .share()
  145. .eraseToAnyPublisher()
  146. registerSubscribers()
  147. registerHandlers()
  148. // Parallelize Setup functions
  149. setupHomeViewConcurrently()
  150. }
  151. private func setupHomeViewConcurrently() {
  152. Task {
  153. // We need to initialize settings and observers first
  154. await self.setupSettings()
  155. await self.setupPumpSettings()
  156. await self.setupCGMSettings()
  157. self.registerObservers()
  158. // The rest can be initialized concurrently
  159. await withTaskGroup(of: Void.self) { group in
  160. group.addTask {
  161. self.setupGlucoseArray()
  162. }
  163. group.addTask {
  164. self.setupCarbsArray()
  165. }
  166. group.addTask {
  167. self.setupFPUsArray()
  168. }
  169. group.addTask {
  170. self.setupDeterminationsArray()
  171. }
  172. group.addTask {
  173. self.setupTDDArray()
  174. }
  175. group.addTask {
  176. self.setupInsulinArray()
  177. }
  178. group.addTask {
  179. self.setupLastBolus()
  180. }
  181. group.addTask {
  182. self.setupBatteryArray()
  183. }
  184. group.addTask {
  185. await self.setupBasalProfile()
  186. }
  187. group.addTask {
  188. await self.setupGlucoseTargets()
  189. }
  190. group.addTask {
  191. self.setupReservoir()
  192. }
  193. group.addTask {
  194. self.setupOverrides()
  195. }
  196. group.addTask {
  197. self.setupOverrideRunStored()
  198. }
  199. group.addTask {
  200. self.setupTempTargetsStored()
  201. }
  202. group.addTask {
  203. self.setupTempTargetsRunStored()
  204. }
  205. group.addTask {
  206. self.iobService.updateIOB()
  207. }
  208. }
  209. }
  210. }
  211. // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
  212. private func registerSubscribers() {
  213. iobService.iobPublisher
  214. .receive(on: DispatchQueue.main)
  215. .sink { [weak self] _ in
  216. guard let self = self else { return }
  217. self.currentIOB = self.iobService.currentIOB ?? 0
  218. }
  219. .store(in: &subscriptions)
  220. glucoseStorage.updatePublisher
  221. .receive(on: queue)
  222. .sink { [weak self] _ in
  223. guard let self = self else { return }
  224. self.setupGlucoseArray()
  225. }
  226. .store(in: &subscriptions)
  227. carbsStorage.updatePublisher
  228. .receive(on: queue)
  229. .sink { [weak self] _ in
  230. guard let self = self else { return }
  231. self.setupFPUsArray()
  232. }
  233. .store(in: &subscriptions)
  234. }
  235. private func registerHandlers() {
  236. coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
  237. guard let self = self else { return }
  238. self.setupDeterminationsArray()
  239. }.store(in: &subscriptions)
  240. coreDataPublisher?.filteredByEntityName("TDDStored").sink { [weak self] _ in
  241. guard let self = self else { return }
  242. self.setupTDDArray()
  243. }.store(in: &subscriptions)
  244. coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
  245. guard let self = self else { return }
  246. self.setupGlucoseArray()
  247. }.store(in: &subscriptions)
  248. coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
  249. guard let self = self else { return }
  250. self.setupCarbsArray()
  251. }.store(in: &subscriptions)
  252. coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
  253. guard let self = self else { return }
  254. self.setupInsulinArray()
  255. self.setupLastBolus()
  256. self.displayPumpStatusHighlightMessage()
  257. self.displayPumpStatusBadge()
  258. }.store(in: &subscriptions)
  259. coreDataPublisher?.filteredByEntityName("OpenAPS_Battery").sink { [weak self] _ in
  260. guard let self = self else { return }
  261. self.setupBatteryArray()
  262. }.store(in: &subscriptions)
  263. coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
  264. guard let self = self else { return }
  265. self.setupOverrides()
  266. }.store(in: &subscriptions)
  267. coreDataPublisher?.filteredByEntityName("OverrideRunStored").sink { [weak self] _ in
  268. guard let self = self else { return }
  269. self.setupOverrideRunStored()
  270. }.store(in: &subscriptions)
  271. coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
  272. guard let self = self else { return }
  273. self.setupTempTargetsStored()
  274. }.store(in: &subscriptions)
  275. coreDataPublisher?.filteredByEntityName("TempTargetRunStored").sink { [weak self] _ in
  276. guard let self = self else { return }
  277. self.setupTempTargetsRunStored()
  278. }.store(in: &subscriptions)
  279. }
  280. private func registerObservers() {
  281. broadcaster.register(DeterminationObserver.self, observer: self)
  282. broadcaster.register(SettingsObserver.self, observer: self)
  283. broadcaster.register(PreferencesObserver.self, observer: self)
  284. broadcaster.register(PumpSettingsObserver.self, observer: self)
  285. broadcaster.register(BasalProfileObserver.self, observer: self)
  286. broadcaster.register(BGTargetsObserver.self, observer: self)
  287. broadcaster.register(PumpReservoirObserver.self, observer: self)
  288. broadcaster.register(PumpDeactivatedObserver.self, observer: self)
  289. timer.eventHandler = {
  290. DispatchQueue.main.async { [weak self] in
  291. self?.timerDate = Date()
  292. }
  293. }
  294. timer.resume()
  295. apsManager.isLooping
  296. .receive(on: DispatchQueue.main)
  297. .weakAssign(to: \.isLooping, on: self)
  298. .store(in: &lifetime)
  299. apsManager.lastLoopDateSubject
  300. .receive(on: DispatchQueue.main)
  301. .weakAssign(to: \.lastLoopDate, on: self)
  302. .store(in: &lifetime)
  303. apsManager.pumpName
  304. .receive(on: DispatchQueue.main)
  305. .weakAssign(to: \.pumpName, on: self)
  306. .store(in: &lifetime)
  307. apsManager.pumpExpiresAtDate
  308. .receive(on: DispatchQueue.main)
  309. .weakAssign(to: \.pumpExpiresAtDate, on: self)
  310. .store(in: &lifetime)
  311. apsManager.lastError
  312. .receive(on: DispatchQueue.main)
  313. .map { [weak self] error in
  314. self?.errorDate = error == nil ? nil : Date()
  315. if let error = error {
  316. info(.default, String(describing: error), notificationText: error.localizedDescription)
  317. }
  318. return error?.localizedDescription
  319. }
  320. .weakAssign(to: \.errorMessage, on: self)
  321. .store(in: &lifetime)
  322. apsManager.bolusProgress
  323. .receive(on: DispatchQueue.main)
  324. .weakAssign(to: \.bolusProgress, on: self)
  325. .store(in: &lifetime)
  326. apsManager.pumpDisplayState
  327. .receive(on: DispatchQueue.main)
  328. .sink { [weak self] state in
  329. guard let self = self else { return }
  330. self.pumpDisplayState = state
  331. if state == nil {
  332. self.reservoir = nil
  333. self.battery = nil
  334. self.pumpName = ""
  335. self.pumpExpiresAtDate = nil
  336. self.shouldDisplayPumpSetupSheet = false
  337. } else {
  338. self.setupReservoir()
  339. self.displayPumpStatusHighlightMessage()
  340. self.displayPumpStatusBadge()
  341. self.setupBatteryArray()
  342. }
  343. }
  344. .store(in: &lifetime)
  345. }
  346. private enum SettingType {
  347. case basal
  348. case carbRatio
  349. case bgTarget
  350. case isf
  351. }
  352. @MainActor private func setupSettings() async {
  353. units = settingsManager.settings.units
  354. allowManualTemp = !settingsManager.settings.closedLoop
  355. closedLoop = settingsManager.settings.closedLoop
  356. lastLoopDate = apsManager.lastLoopDate
  357. alarm = provider.glucoseStorage.alarm
  358. manualTempBasal = apsManager.isManualTempBasal
  359. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  360. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  361. autosensMax = settingsManager.preferences.autosensMax
  362. lowGlucose = settingsManager.settings.low
  363. highGlucose = settingsManager.settings.high
  364. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  365. displayXgridLines = settingsManager.settings.xGridLines
  366. displayYgridLines = settingsManager.settings.yGridLines
  367. thresholdLines = settingsManager.settings.rulerMarks
  368. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  369. forecastDisplayType = settingsManager.settings.forecastDisplayType
  370. isExerciseModeActive = settingsManager.preferences.exerciseMode
  371. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  372. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  373. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  374. maxIOB = settingsManager.preferences.maxIOB
  375. }
  376. @MainActor private func setupCGMSettings() async {
  377. cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
  378. listOfCGM = (
  379. CGMType.allCases.filter { $0 != CGMType.plugin }.map {
  380. CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
  381. } +
  382. pluginCGMManager.availableCGMManagers.map {
  383. CGMModel(
  384. id: $0.identifier,
  385. type: CGMType.plugin,
  386. displayName: $0.localizedTitle,
  387. subtitle: $0.localizedTitle
  388. )
  389. }
  390. ).sorted(by: { lhs, rhs in
  391. if lhs.displayName == "None" {
  392. return true
  393. } else if rhs.displayName == "None" {
  394. return false
  395. } else {
  396. return lhs.displayName < rhs.displayName
  397. }
  398. })
  399. switch settingsManager.settings.cgm {
  400. case .plugin:
  401. if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
  402. cgmCurrent = CGMModel(
  403. id: settingsManager.settings.cgmPluginIdentifier,
  404. type: .plugin,
  405. displayName: cgmPluginInfo.displayName,
  406. subtitle: cgmPluginInfo.subtitle
  407. )
  408. } else {
  409. // no more type of plugin available - fallback to default
  410. cgmCurrent = cgmDefaultModel
  411. }
  412. default:
  413. cgmCurrent = CGMModel(
  414. id: settingsManager.settings.cgm.id,
  415. type: settingsManager.settings.cgm,
  416. displayName: settingsManager.settings.cgm.displayName,
  417. subtitle: settingsManager.settings.cgm.subtitle
  418. )
  419. }
  420. }
  421. func addPump(_ type: PumpConfig.PumpType) {
  422. setupPumpType = type
  423. shouldDisplayPumpSetupSheet = true
  424. }
  425. func addCGM(cgm: CGMModel) {
  426. cgmCurrent = cgm
  427. switch cgmCurrent.type {
  428. case .plugin:
  429. shouldDisplayCGMSetupSheet = true
  430. default:
  431. shouldDisplayCGMSetupSheet = true
  432. settingsManager.settings.cgm = cgmCurrent.type
  433. settingsManager.settings.cgmPluginIdentifier = ""
  434. fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
  435. broadcaster.notify(GlucoseObserver.self, on: .main) {
  436. $0.glucoseDidUpdate([])
  437. }
  438. }
  439. }
  440. func deleteCGM() {
  441. fetchGlucoseManager.performOnCGMManagerQueue {
  442. // Call plugin functionality on the manager queue (or at least attempt to)
  443. Task {
  444. await self.fetchGlucoseManager?.deleteGlucoseSource()
  445. // UI updates go back to Main
  446. await MainActor.run {
  447. self.shouldDisplayCGMSetupSheet = false
  448. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  449. $0.glucoseDidUpdate([])
  450. }
  451. }
  452. }
  453. }
  454. }
  455. /// Display the eventual status message provided by the manager of the pump
  456. /// Only display if state is warning or critical message else return nil
  457. private func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
  458. DispatchQueue.main.async { [weak self] in
  459. guard let self = self else { return }
  460. if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
  461. statusHighlight.state == .warning || statusHighlight.state == .critical, !didDeactivate
  462. {
  463. pumpStatusHighlightMessage = (statusHighlight.state == .warning ? "⚠️\n" : "‼️\n") + statusHighlight
  464. .localizedMessage
  465. } else {
  466. pumpStatusHighlightMessage = nil
  467. }
  468. }
  469. }
  470. private func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
  471. DispatchQueue.main.async { [weak self] in
  472. guard let self = self else { return }
  473. if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,
  474. let image = statusBadge.image, !didDeactivate
  475. {
  476. pumpStatusBadgeImage = image
  477. pumpStatusBadgeColor = statusBadge.state == .critical ? .critical : .warning
  478. } else {
  479. pumpStatusBadgeImage = nil
  480. pumpStatusBadgeColor = nil
  481. }
  482. }
  483. }
  484. func runLoop() {
  485. provider.heartbeatNow()
  486. }
  487. func showProgressView() {
  488. glucoseStorage
  489. .isGlucoseDataFresh(glucoseFromPersistence.first?.date) ? (waitForSuggestion = true) : (waitForSuggestion = false)
  490. }
  491. func cancelBolus() {
  492. Task {
  493. await apsManager.cancelBolus(nil)
  494. // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
  495. try await apsManager.determineBasalSync()
  496. }
  497. }
  498. private func setupPumpSettings() async {
  499. let settings = await provider.pumpSettings()
  500. await MainActor.run {
  501. self.maxBasal = settings.maxBasal
  502. self.pumpInitialSettings.maxBasalRateUnitsPerHour = Double(settings.maxBasal)
  503. self.pumpInitialSettings.maxBolusUnits = Double(settings.maxBolus)
  504. }
  505. }
  506. private func setupBasalProfile() async {
  507. let basalProfile = await provider.getBasalProfile()
  508. await MainActor.run {
  509. self.basalProfile = basalProfile
  510. if let schedule = BasalRateSchedule(
  511. dailyItems: basalProfile
  512. .map { RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate)) }
  513. ) {
  514. self.pumpInitialSettings.basalSchedule = schedule
  515. }
  516. }
  517. }
  518. private func setupGlucoseTargets() async {
  519. let bgTargets = await provider.getBGTargets()
  520. let targetProfiles = processFetchedTargets(bgTargets, startMarker: startMarker)
  521. await MainActor.run {
  522. self.bgTargets = bgTargets
  523. self.targetProfiles = targetProfiles
  524. }
  525. }
  526. private func setupReservoir() {
  527. Task {
  528. let reservoir = await provider.pumpReservoir()
  529. await MainActor.run {
  530. self.reservoir = reservoir
  531. }
  532. }
  533. }
  534. private func getCurrentGlucoseTarget() async {
  535. let now = Date()
  536. let calendar = Calendar.current
  537. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  538. for (index, entry) in entries.enumerated() {
  539. guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
  540. debug(.default, "Invalid entry start time: \(entry.start)")
  541. continue
  542. }
  543. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  544. let entryStartTime = calendar.date(
  545. bySettingHour: entryComponents.hour!,
  546. minute: entryComponents.minute!,
  547. second: entryComponents.second!,
  548. of: now
  549. )!
  550. let entryEndTime: Date
  551. if index < entries.count - 1,
  552. let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
  553. {
  554. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  555. entryEndTime = calendar.date(
  556. bySettingHour: nextEntryComponents.hour!,
  557. minute: nextEntryComponents.minute!,
  558. second: nextEntryComponents.second!,
  559. of: now
  560. )!
  561. } else {
  562. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  563. }
  564. if now >= entryStartTime, now < entryEndTime {
  565. await MainActor.run {
  566. currentGlucoseTarget = entry.value
  567. }
  568. return
  569. }
  570. }
  571. }
  572. }
  573. }
  574. extension Home.StateModel:
  575. DeterminationObserver,
  576. SettingsObserver,
  577. PreferencesObserver,
  578. PumpSettingsObserver,
  579. BasalProfileObserver,
  580. BGTargetsObserver,
  581. PumpReservoirObserver,
  582. PumpDeactivatedObserver
  583. {
  584. func determinationDidUpdate(_: Determination) {
  585. waitForSuggestion = false
  586. }
  587. func settingsDidChange(_ settings: TrioSettings) {
  588. allowManualTemp = !settings.closedLoop
  589. closedLoop = settingsManager.settings.closedLoop
  590. units = settingsManager.settings.units
  591. manualTempBasal = apsManager.isManualTempBasal
  592. isSmoothingEnabled = settingsManager.settings.smoothGlucose
  593. lowGlucose = settingsManager.settings.low
  594. highGlucose = settingsManager.settings.high
  595. Task {
  596. await getCurrentGlucoseTarget()
  597. await setupGlucoseTargets()
  598. }
  599. eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
  600. glucoseColorScheme = settingsManager.settings.glucoseColorScheme
  601. displayXgridLines = settingsManager.settings.xGridLines
  602. displayYgridLines = settingsManager.settings.yGridLines
  603. thresholdLines = settingsManager.settings.rulerMarks
  604. showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
  605. forecastDisplayType = settingsManager.settings.forecastDisplayType
  606. cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
  607. displayPumpStatusHighlightMessage()
  608. displayPumpStatusBadge()
  609. setupBatteryArray()
  610. Task {
  611. await setupCGMSettings()
  612. }
  613. if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
  614. shouldRunDeleteOnSettingsChange = false
  615. cgmCurrent = cgmDefaultModel
  616. DispatchQueue.main.async {
  617. self.broadcaster.notify(GlucoseObserver.self, on: .main) {
  618. $0.glucoseDidUpdate([])
  619. }
  620. }
  621. } else {
  622. shouldRunDeleteOnSettingsChange = true
  623. }
  624. }
  625. func preferencesDidChange(_: Preferences) {
  626. autosensMax = settingsManager.preferences.autosensMax
  627. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  628. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  629. isExerciseModeActive = settingsManager.preferences.exerciseMode
  630. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  631. maxIOB = settingsManager.preferences.maxIOB
  632. }
  633. func pumpSettingsDidChange(_: PumpSettings) {
  634. Task {
  635. await setupPumpSettings()
  636. setupBatteryArray()
  637. }
  638. }
  639. func basalProfileDidChange(_: [BasalProfileEntry]) {
  640. Task {
  641. await setupBasalProfile()
  642. }
  643. }
  644. func bgTargetsDidChange(_: BGTargets) {
  645. Task {
  646. await setupGlucoseTargets()
  647. }
  648. }
  649. func pumpReservoirDidChange(_: Decimal) {
  650. setupReservoir()
  651. displayPumpStatusHighlightMessage()
  652. displayPumpStatusBadge()
  653. }
  654. func pumpDeactivatedDidChange() {
  655. displayPumpStatusHighlightMessage(true)
  656. displayPumpStatusBadge(true)
  657. batteryFromPersistence = []
  658. }
  659. }
  660. extension Home.StateModel: PumpManagerOnboardingDelegate {
  661. func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
  662. provider.apsManager.pumpManager = pumpManager
  663. if let insulinType = pumpManager.status.insulinType {
  664. settingsManager.updateInsulinCurve(insulinType)
  665. }
  666. }
  667. func pumpManagerOnboarding(didOnboardPumpManager _: PumpManagerUI) {
  668. // nothing to do
  669. }
  670. func pumpManagerOnboarding(didPauseOnboarding _: PumpManagerUI) {
  671. // nothing to do
  672. }
  673. }