AdjustmentsStateModel.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import Combine
  2. import CoreData
  3. import Observation
  4. import SwiftUI
  5. extension Adjustments {
  6. @Observable final class StateModel: BaseStateModel<Provider> {
  7. // MARK: - Injected Dependencies
  8. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  9. @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
  10. @ObservationIgnored @Injected() var apsManager: APSManager!
  11. @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
  12. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  13. // MARK: - Override and Temp Target Properties
  14. var overridePercentage: Double = 100
  15. var isEnabled = false
  16. var indefinite = true
  17. var overrideDuration: Decimal = 0
  18. var target: Decimal = 0
  19. var currentGlucoseTarget: Decimal = 100
  20. var shouldOverrideTarget: Bool = false
  21. var smbIsOff: Bool = false
  22. var id = ""
  23. var overrideName: String = ""
  24. var isPreset: Bool = false
  25. var overridePresets: [OverrideStored] = []
  26. var advancedSettings: Bool = false
  27. var isfAndCr: Bool = true
  28. var isf: Bool = true
  29. var cr: Bool = true
  30. var smbIsScheduledOff: Bool = false
  31. var start: Decimal = 0
  32. var end: Decimal = 0
  33. var smbMinutes: Decimal = 0
  34. var uamMinutes: Decimal = 0
  35. var defaultSmbMinutes: Decimal = 0
  36. var defaultUamMinutes: Decimal = 0
  37. var selectedTab: Tab = .overrides
  38. var activeOverrideName: String = ""
  39. var currentActiveOverride: OverrideStored?
  40. var activeTempTargetName: String = ""
  41. var currentActiveTempTarget: TempTargetStored?
  42. var showOverrideEditSheet = false
  43. var showTempTargetEditSheet = false
  44. var units: GlucoseUnits = .mgdL
  45. // Temp Target Properties
  46. let normalTarget: Decimal = 100
  47. var tempTargetDuration: Decimal = 0
  48. var tempTargetName: String = ""
  49. var tempTargetTarget: Decimal = 100
  50. var isTempTargetEnabled: Bool = false
  51. var date = Date()
  52. var newPresetName = ""
  53. var tempTargetPresets: [TempTargetStored] = []
  54. var scheduledTempTargets: [TempTargetStored] = []
  55. var percentage: Double = 100
  56. var maxValue: Decimal = 1.2
  57. var halfBasalTarget: Decimal = 160
  58. var settingHalfBasalTarget: Decimal = 160
  59. var highTTraisesSens: Bool = false
  60. var isExerciseModeActive: Bool = false
  61. var lowTTlowersSens: Bool = false
  62. var didSaveSettings: Bool = false
  63. // Core Data
  64. let coredataContext = CoreDataStack.shared.newTaskContext()
  65. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  66. // Help Sheet
  67. var isHelpSheetPresented: Bool = false
  68. var helpSheetDetent = PresentationDetent.large
  69. // Combine
  70. private var cancellables = Set<AnyCancellable>()
  71. // MARK: - Lifecycle
  72. /// Subscribes to notifications and initializes settings.
  73. override func subscribe() {
  74. setupNotification()
  75. setupSettings()
  76. broadcaster.register(SettingsObserver.self, observer: self)
  77. broadcaster.register(PreferencesObserver.self, observer: self)
  78. Task {
  79. await withTaskGroup(of: Void.self) { group in
  80. group.addTask { self.setupOverridePresetsArray() }
  81. group.addTask { self.setupTempTargetPresetsArray() }
  82. group.addTask { self.updateLatestOverrideConfiguration() }
  83. group.addTask { self.updateLatestTempTargetConfiguration() }
  84. }
  85. }
  86. }
  87. /// Retrieves the current glucose target based on the time of day.
  88. func getCurrentGlucoseTarget() async {
  89. let now = Date()
  90. let calendar = Calendar.current
  91. let dateFormatter = DateFormatter()
  92. dateFormatter.dateFormat = "HH:mm:ss"
  93. dateFormatter.timeZone = TimeZone.current
  94. let bgTargets = await provider.getBGTargets()
  95. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  96. for (index, entry) in entries.enumerated() {
  97. guard let entryTime = dateFormatter.date(from: entry.start) else {
  98. print("Invalid entry start time: \(entry.start)")
  99. continue
  100. }
  101. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  102. let entryStartTime = calendar.date(
  103. bySettingHour: entryComponents.hour!,
  104. minute: entryComponents.minute!,
  105. second: entryComponents.second!,
  106. of: now
  107. )!
  108. let entryEndTime: Date
  109. if index < entries.count - 1,
  110. let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
  111. {
  112. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  113. entryEndTime = calendar.date(
  114. bySettingHour: nextEntryComponents.hour!,
  115. minute: nextEntryComponents.minute!,
  116. second: nextEntryComponents.second!,
  117. of: now
  118. )!
  119. } else {
  120. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  121. }
  122. if now >= entryStartTime, now < entryEndTime {
  123. await MainActor.run {
  124. currentGlucoseTarget = entry.value
  125. target = currentGlucoseTarget
  126. }
  127. return
  128. }
  129. }
  130. }
  131. /// Configures various settings from the settings manager.
  132. private func setupSettings() {
  133. units = settingsManager.settings.units
  134. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  135. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  136. maxValue = settingsManager.preferences.autosensMax
  137. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  138. halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  139. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  140. isExerciseModeActive = settingsManager.preferences.exerciseMode
  141. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  142. percentage = computeAdjustedPercentage()
  143. Task {
  144. await getCurrentGlucoseTarget()
  145. }
  146. }
  147. /// Reorders Override Presets and updates the view.
  148. func reorderOverride(from source: IndexSet, to destination: Int) {
  149. overridePresets.move(fromOffsets: source, toOffset: destination)
  150. for (index, override) in overridePresets.enumerated() {
  151. override.orderPosition = Int16(index + 1)
  152. }
  153. do {
  154. guard viewContext.hasChanges else { return }
  155. try viewContext.save()
  156. setupOverridePresetsArray()
  157. Task { await nightscoutManager.uploadProfiles() }
  158. } catch {
  159. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Presets order")
  160. }
  161. }
  162. /// Reorders Temp Target Presets and updates the view.
  163. func reorderTempTargets(from source: IndexSet, to destination: Int) {
  164. tempTargetPresets.move(fromOffsets: source, toOffset: destination)
  165. for (index, tempTarget) in tempTargetPresets.enumerated() {
  166. tempTarget.orderPosition = Int16(index + 1)
  167. }
  168. do {
  169. guard viewContext.hasChanges else { return }
  170. try viewContext.save()
  171. setupTempTargetPresetsArray()
  172. } catch {
  173. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target Presets order")
  174. }
  175. }
  176. }
  177. }
  178. // MARK: - Notifications Setup
  179. extension Adjustments.StateModel {
  180. /// Sets up notification observers for Override and Temp Target updates.
  181. func setupNotification() {
  182. Foundation.NotificationCenter.default.addObserver(
  183. self,
  184. selector: #selector(handleOverrideConfigurationUpdate),
  185. name: .didUpdateOverrideConfiguration,
  186. object: nil
  187. )
  188. // Custom Notification to update View when an Temp Target has been cancelled via Home View
  189. Foundation.NotificationCenter.default.addObserver(
  190. self,
  191. selector: #selector(handleTempTargetConfigurationUpdate),
  192. name: .didUpdateTempTargetConfiguration,
  193. object: nil
  194. )
  195. // Creates a publisher that updates the Override View when the Custom notification was sent (via shortcut)
  196. Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
  197. .sink { [weak self] _ in
  198. guard let self = self else { return }
  199. self.updateLatestOverrideConfiguration()
  200. }
  201. .store(in: &cancellables)
  202. // Creates a publisher that updates the Temp Target View when the Custom notification was sent (via shortcut)
  203. Foundation.NotificationCenter.default.publisher(for: .willUpdateTempTargetConfiguration)
  204. .sink { [weak self] _ in
  205. guard let self = self else { return }
  206. self.updateLatestTempTargetConfiguration()
  207. }
  208. .store(in: &cancellables)
  209. }
  210. /// Handles Override configuration updates.
  211. @objc private func handleOverrideConfigurationUpdate() {
  212. updateLatestOverrideConfiguration()
  213. }
  214. /// Handles Temp Target configuration updates.
  215. @objc private func handleTempTargetConfigurationUpdate() {
  216. updateLatestTempTargetConfiguration()
  217. }
  218. }
  219. extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
  220. /// Updates settings when they change.
  221. func settingsDidChange(_: FreeAPSSettings) {
  222. units = settingsManager.settings.units
  223. Task {
  224. await getCurrentGlucoseTarget()
  225. }
  226. }
  227. /// Updates preferences when they change.
  228. func preferencesDidChange(_: Preferences) {
  229. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  230. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  231. maxValue = settingsManager.preferences.autosensMax
  232. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  233. halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  234. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  235. isExerciseModeActive = settingsManager.preferences.exerciseMode
  236. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  237. percentage = computeAdjustedPercentage()
  238. Task {
  239. await getCurrentGlucoseTarget()
  240. }
  241. }
  242. }