HomeStateModel.swift 30 KB

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