AdjustmentsStateModel.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import Combine
  2. import CoreData
  3. import Observation
  4. import SwiftUI
  5. extension Adjustments {
  6. @Observable final class StateModel: BaseStateModel<Provider> {
  7. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  8. @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
  9. @ObservationIgnored @Injected() var apsManager: APSManager!
  10. @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
  11. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  12. var overridePercentage: Double = 100
  13. var isEnabled = false
  14. var indefinite = true
  15. var overrideDuration: Decimal = 0
  16. var target: Decimal = 0
  17. var currentGlucoseTarget: Decimal = 100
  18. var shouldOverrideTarget: Bool = false
  19. var smbIsOff: Bool = false
  20. var id = ""
  21. var overrideName: String = ""
  22. var isPreset: Bool = false
  23. var overridePresets: [OverrideStored] = []
  24. var advancedSettings: Bool = false
  25. var isfAndCr: Bool = true
  26. var isf: Bool = true
  27. var cr: Bool = true
  28. var smbIsScheduledOff: Bool = false
  29. var start: Decimal = 0
  30. var end: Decimal = 0
  31. var smbMinutes: Decimal = 0
  32. var uamMinutes: Decimal = 0
  33. var defaultSmbMinutes: Decimal = 0
  34. var defaultUamMinutes: Decimal = 0
  35. var selectedTab: Tab = .overrides
  36. var activeOverrideName: String = ""
  37. var currentActiveOverride: OverrideStored?
  38. var activeTempTargetName: String = ""
  39. var currentActiveTempTarget: TempTargetStored?
  40. var showOverrideEditSheet = false
  41. var showTempTargetEditSheet = false
  42. var units: GlucoseUnits = .mgdL
  43. // temp target stuff
  44. let normalTarget: Decimal = 100
  45. var tempTargetDuration: Decimal = 0
  46. var tempTargetName: String = ""
  47. var tempTargetTarget: Decimal = 100
  48. var isTempTargetEnabled: Bool = false
  49. var date = Date()
  50. var newPresetName = ""
  51. var tempTargetPresets: [TempTargetStored] = []
  52. var scheduledTempTargets: [TempTargetStored] = []
  53. var percentage: Double = 100
  54. var maxValue: Decimal = 1.2
  55. var halfBasalTarget: Decimal = 160
  56. var settingHalfBasalTarget: Decimal = 160
  57. var highTTraisesSens: Bool = false
  58. var isExerciseModeActive: Bool = false
  59. var lowTTlowersSens: Bool = false
  60. var didSaveSettings: Bool = false
  61. let coredataContext = CoreDataStack.shared.newTaskContext()
  62. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  63. var isHelpSheetPresented: Bool = false
  64. var helpSheetDetent = PresentationDetent.large
  65. private var cancellables = Set<AnyCancellable>()
  66. override func subscribe() {
  67. setupNotification()
  68. setupSettings()
  69. broadcaster.register(SettingsObserver.self, observer: self)
  70. broadcaster.register(PreferencesObserver.self, observer: self)
  71. Task {
  72. await withTaskGroup(of: Void.self) { group in
  73. group.addTask {
  74. self.setupOverridePresetsArray()
  75. }
  76. group.addTask {
  77. self.setupTempTargetPresetsArray()
  78. }
  79. group.addTask {
  80. self.updateLatestOverrideConfiguration()
  81. }
  82. group.addTask {
  83. self.updateLatestTempTargetConfiguration()
  84. }
  85. }
  86. }
  87. }
  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.getBGTarget()
  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. private func setupSettings() {
  132. units = settingsManager.settings.units
  133. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  134. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  135. maxValue = settingsManager.preferences.autosensMax
  136. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  137. halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  138. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  139. isExerciseModeActive = settingsManager.preferences.exerciseMode
  140. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  141. percentage = computeAdjustedPercentage()
  142. Task {
  143. await getCurrentGlucoseTarget()
  144. }
  145. }
  146. }
  147. }
  148. // MARK: - Setup Notifications
  149. extension Adjustments.StateModel {
  150. // Custom Notification to update View when an Override has been cancelled via Home View
  151. func setupNotification() {
  152. Foundation.NotificationCenter.default.addObserver(
  153. self,
  154. selector: #selector(handleOverrideConfigurationUpdate),
  155. name: .didUpdateOverrideConfiguration,
  156. object: nil
  157. )
  158. // Custom Notification to update View when an Temp Target has been cancelled via Home View
  159. Foundation.NotificationCenter.default.addObserver(
  160. self,
  161. selector: #selector(handleTempTargetConfigurationUpdate),
  162. name: .didUpdateTempTargetConfiguration,
  163. object: nil
  164. )
  165. // Creates a publisher that updates the Override View when the Custom notification was sent (via shortcut)
  166. Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
  167. .sink { [weak self] _ in
  168. guard let self = self else { return }
  169. self.updateLatestOverrideConfiguration()
  170. }
  171. .store(in: &cancellables)
  172. // Creates a publisher that updates the Temp Target View when the Custom notification was sent (via shortcut)
  173. Foundation.NotificationCenter.default.publisher(for: .willUpdateTempTargetConfiguration)
  174. .sink { [weak self] _ in
  175. guard let self = self else { return }
  176. self.updateLatestTempTargetConfiguration()
  177. }
  178. .store(in: &cancellables)
  179. }
  180. @objc private func handleOverrideConfigurationUpdate() {
  181. updateLatestOverrideConfiguration()
  182. }
  183. @objc private func handleTempTargetConfigurationUpdate() {
  184. updateLatestTempTargetConfiguration()
  185. }
  186. func reorderOverride(from source: IndexSet, to destination: Int) {
  187. overridePresets.move(fromOffsets: source, toOffset: destination)
  188. for (index, override) in overridePresets.enumerated() {
  189. override.orderPosition = Int16(index + 1)
  190. }
  191. do {
  192. guard viewContext.hasChanges else { return }
  193. try viewContext.save()
  194. // Update Presets View
  195. setupOverridePresetsArray()
  196. Task {
  197. await nightscoutManager.uploadProfiles()
  198. }
  199. } catch {
  200. debugPrint(
  201. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
  202. )
  203. }
  204. }
  205. func reorderTempTargets(from source: IndexSet, to destination: Int) {
  206. tempTargetPresets.move(fromOffsets: source, toOffset: destination)
  207. for (index, tempTarget) in tempTargetPresets.enumerated() {
  208. tempTarget.orderPosition = Int16(index + 1)
  209. }
  210. do {
  211. guard viewContext.hasChanges else { return }
  212. try viewContext.save()
  213. // Update Presets View
  214. setupTempTargetPresetsArray()
  215. } catch {
  216. debugPrint(
  217. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Temp Target Presets with error: \(error.localizedDescription)"
  218. )
  219. }
  220. }
  221. }
  222. extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
  223. func settingsDidChange(_: FreeAPSSettings) {
  224. units = settingsManager.settings.units
  225. Task {
  226. await getCurrentGlucoseTarget()
  227. }
  228. }
  229. func preferencesDidChange(_: Preferences) {
  230. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  231. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  232. maxValue = settingsManager.preferences.autosensMax
  233. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  234. halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  235. highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
  236. isExerciseModeActive = settingsManager.preferences.exerciseMode
  237. lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
  238. percentage = computeAdjustedPercentage()
  239. Task {
  240. await getCurrentGlucoseTarget()
  241. }
  242. }
  243. }
  244. extension PickerSettingsProvider {
  245. func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits, roundMinToStep: Bool) -> [Decimal] {
  246. if !roundMinToStep {
  247. return generatePickerValues(from: setting, units: units)
  248. }
  249. // Adjust min to be divisible by step
  250. var newSetting = setting
  251. var min = Double(newSetting.min)
  252. let step = Double(newSetting.step)
  253. let remainder = min.truncatingRemainder(dividingBy: step)
  254. if remainder != 0 {
  255. // Move min up to the next value divisible by targetStep
  256. min += (step - remainder)
  257. }
  258. newSetting.min = Decimal(min)
  259. return generatePickerValues(from: newSetting, units: units)
  260. }
  261. }
  262. func percentageDescription(_ percent: Double) -> Text? {
  263. if percent.isNaN || percent == 100 { return nil }
  264. var description: String = "Insulin doses will be "
  265. if percent < 100 {
  266. description += "decreased by "
  267. } else {
  268. description += "increased by "
  269. }
  270. let deviationFrom100 = abs(percent - 100)
  271. description += String(format: "%.0f% %.", deviationFrom100)
  272. return Text(description)
  273. }
  274. // Function to check if the phone is using 24-hour format
  275. func is24HourFormat() -> Bool {
  276. let formatter = DateFormatter()
  277. formatter.locale = Locale.current
  278. formatter.dateStyle = .none
  279. formatter.timeStyle = .short
  280. let dateString = formatter.string(from: Date())
  281. return !dateString.contains("AM") && !dateString.contains("PM")
  282. }
  283. // Helper function to convert hours to AM/PM format
  284. func convertTo12HourFormat(_ hour: Int) -> String {
  285. let formatter = DateFormatter()
  286. formatter.dateFormat = "h a"
  287. // Create a date from the hour and format it to AM/PM
  288. let calendar = Calendar.current
  289. let components = DateComponents(hour: hour)
  290. let date = calendar.date(from: components) ?? Date()
  291. return formatter.string(from: date)
  292. }
  293. // Helper function to format 24-hour numbers as two digits
  294. func format24Hour(_ hour: Int) -> String {
  295. String(format: "%02d", hour)
  296. }
  297. func formatHrMin(_ durationInMinutes: Int) -> String {
  298. let hours = durationInMinutes / 60
  299. let minutes = durationInMinutes % 60
  300. switch (hours, minutes) {
  301. case let (0, m):
  302. return "\(m) min"
  303. case let (h, 0):
  304. return "\(h) hr"
  305. default:
  306. return "\(hours) hr \(minutes) min"
  307. }
  308. }
  309. func convertToMinutes(_ hours: Int, _ minutes: Int) -> Decimal {
  310. let totalMinutes = (hours * 60) + minutes
  311. return Decimal(max(0, totalMinutes))
  312. }