OverrideStateModel.swift 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098
  1. import Combine
  2. import CoreData
  3. import Observation
  4. import SwiftUI
  5. extension OverrideConfig {
  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 showInvalidTargetAlert = false
  43. var units: GlucoseUnits = .mgdL
  44. // temp target stuff
  45. let normalTarget: Decimal = 100
  46. var tempTargetDuration: Decimal = 0
  47. var tempTargetName: String = ""
  48. var tempTargetTarget: Decimal = 100
  49. var isTempTargetEnabled: Bool = false
  50. var date = Date()
  51. var newPresetName = ""
  52. var tempTargetPresets: [TempTargetStored] = []
  53. var percentage = 100.0
  54. var maxValue: Decimal = 1.2
  55. var minValue: Decimal = 0.15
  56. var viewPercantage = false
  57. var halfBasalTarget: Decimal = 160
  58. var settingHalfBasalTarget: Decimal = 160
  59. var didSaveSettings: Bool = false
  60. var didAdjustSens: Bool = false {
  61. didSet {
  62. handleAdjustSensToggle()
  63. }
  64. }
  65. let coredataContext = CoreDataStack.shared.newTaskContext()
  66. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  67. var isHelpSheetPresented: Bool = false
  68. var helpSheetDetent = PresentationDetent.large
  69. var alertMessage: String {
  70. let target: String = units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
  71. return "Please enter a valid target between" + " \(target)."
  72. }
  73. private var cancellables = Set<AnyCancellable>()
  74. override func subscribe() {
  75. setupNotification()
  76. setupSettings()
  77. broadcaster.register(SettingsObserver.self, observer: self)
  78. Task {
  79. await withTaskGroup(of: Void.self) { group in
  80. group.addTask {
  81. self.setupOverridePresetsArray()
  82. }
  83. group.addTask {
  84. self.setupTempTargetPresetsArray()
  85. }
  86. group.addTask {
  87. self.updateLatestOverrideConfiguration()
  88. }
  89. group.addTask {
  90. self.updateLatestTempTargetConfiguration()
  91. }
  92. }
  93. }
  94. }
  95. func getCurrentGlucoseTarget() async {
  96. let now = Date()
  97. let calendar = Calendar.current
  98. let dateFormatter = DateFormatter()
  99. dateFormatter.dateFormat = "HH:mm:ss"
  100. dateFormatter.timeZone = TimeZone.current
  101. let bgTargets = await provider.getBGTarget()
  102. let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
  103. for (index, entry) in entries.enumerated() {
  104. guard let entryTime = dateFormatter.date(from: entry.start) else {
  105. print("Invalid entry start time: \(entry.start)")
  106. continue
  107. }
  108. let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
  109. let entryStartTime = calendar.date(
  110. bySettingHour: entryComponents.hour!,
  111. minute: entryComponents.minute!,
  112. second: entryComponents.second!,
  113. of: now
  114. )!
  115. let entryEndTime: Date
  116. if index < entries.count - 1,
  117. let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
  118. {
  119. let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
  120. entryEndTime = calendar.date(
  121. bySettingHour: nextEntryComponents.hour!,
  122. minute: nextEntryComponents.minute!,
  123. second: nextEntryComponents.second!,
  124. of: now
  125. )!
  126. } else {
  127. entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
  128. }
  129. if now >= entryStartTime, now < entryEndTime {
  130. await MainActor.run {
  131. currentGlucoseTarget = units == .mgdL ? entry.value : entry.value.asMmolL
  132. target = currentGlucoseTarget
  133. }
  134. return
  135. }
  136. }
  137. }
  138. private func setupSettings() {
  139. units = settingsManager.settings.units
  140. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  141. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  142. maxValue = settingsManager.preferences.autosensMax
  143. minValue = settingsManager.preferences.autosensMin
  144. settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  145. halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  146. percentage = Double(computeAdjustedPercentage() * 100)
  147. Task {
  148. await getCurrentGlucoseTarget()
  149. }
  150. }
  151. func isInputInvalid(target: Decimal) -> Bool {
  152. guard target != 0 else { return false }
  153. if target < 80 || target > 270 // in oref min lowTT = 80!
  154. {
  155. showInvalidTargetAlert = true
  156. return true
  157. } else {
  158. return false
  159. }
  160. }
  161. }
  162. }
  163. // MARK: - Setup Notifications
  164. extension OverrideConfig.StateModel {
  165. // Custom Notification to update View when an Override has been cancelled via Home View
  166. func setupNotification() {
  167. Foundation.NotificationCenter.default.addObserver(
  168. self,
  169. selector: #selector(handleOverrideConfigurationUpdate),
  170. name: .didUpdateOverrideConfiguration,
  171. object: nil
  172. )
  173. // Custom Notification to update View when an Temp Target has been cancelled via Home View
  174. Foundation.NotificationCenter.default.addObserver(
  175. self,
  176. selector: #selector(handleTempTargetConfigurationUpdate),
  177. name: .didUpdateTempTargetConfiguration,
  178. object: nil
  179. )
  180. }
  181. @objc private func handleOverrideConfigurationUpdate() {
  182. updateLatestOverrideConfiguration()
  183. Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
  184. .sink { [weak self] _ in
  185. guard let self = self else { return }
  186. self.updateLatestOverrideConfiguration()
  187. }
  188. .store(in: &cancellables)
  189. }
  190. @objc private func handleTempTargetConfigurationUpdate() {
  191. updateLatestTempTargetConfiguration()
  192. }
  193. // MARK: - Enact Overrides
  194. func reorderOverride(from source: IndexSet, to destination: Int) {
  195. overridePresets.move(fromOffsets: source, toOffset: destination)
  196. for (index, override) in overridePresets.enumerated() {
  197. override.orderPosition = Int16(index + 1)
  198. }
  199. do {
  200. guard viewContext.hasChanges else { return }
  201. try viewContext.save()
  202. // Update Presets View
  203. setupOverridePresetsArray()
  204. Task {
  205. await nightscoutManager.uploadProfiles()
  206. }
  207. } catch {
  208. debugPrint(
  209. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
  210. )
  211. }
  212. }
  213. func reorderTempTargets(from source: IndexSet, to destination: Int) {
  214. tempTargetPresets.move(fromOffsets: source, toOffset: destination)
  215. for (index, tempTarget) in tempTargetPresets.enumerated() {
  216. tempTarget.orderPosition = Int16(index + 1)
  217. }
  218. do {
  219. guard viewContext.hasChanges else { return }
  220. try viewContext.save()
  221. // Update Presets View
  222. setupTempTargetPresetsArray()
  223. } catch {
  224. debugPrint(
  225. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Temp Target Presets with error: \(error.localizedDescription)"
  226. )
  227. }
  228. }
  229. /// here we only have to update the Boolean Flag 'enabled'
  230. @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
  231. do {
  232. /// get the underlying NSManagedObject of the Override that should be enabled
  233. let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
  234. overrideToEnact?.enabled = true
  235. overrideToEnact?.date = Date()
  236. overrideToEnact?.isUploadedToNS = false
  237. /// Update the 'Cancel Override' button state
  238. isEnabled = true
  239. /// disable all active Overrides and reset state variables
  240. /// do not create a OverrideRunEntry because we only want that if we cancel a running Override, not when enacting a Preset
  241. await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
  242. await resetStateVariables()
  243. guard viewContext.hasChanges else { return }
  244. try viewContext.save()
  245. // Update View
  246. updateLatestOverrideConfiguration()
  247. } catch {
  248. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
  249. }
  250. }
  251. // MARK: - Save the Override that we want to cancel to the OverrideRunStored Entity, then cancel ALL active overrides
  252. @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
  253. // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
  254. let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
  255. await viewContext.perform {
  256. do {
  257. // Fetch the existing OverrideStored objects from the context
  258. let results = try ids.compactMap { id in
  259. try self.viewContext.existingObject(with: id) as? OverrideStored
  260. }
  261. // If there are no results, return early
  262. guard !results.isEmpty else { return }
  263. // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
  264. if createOverrideRunEntry {
  265. // Use the first override to create a new OverrideRunStored entry
  266. if let canceledOverride = results.first {
  267. let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
  268. newOverrideRunStored.id = UUID()
  269. newOverrideRunStored.name = canceledOverride.name
  270. newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
  271. newOverrideRunStored.endDate = Date()
  272. newOverrideRunStored
  273. .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
  274. newOverrideRunStored.override = canceledOverride
  275. newOverrideRunStored.isUploadedToNS = false
  276. }
  277. }
  278. // Disable all override except the one with overrideID
  279. for overrideToCancel in results {
  280. if overrideToCancel.objectID != overrideID {
  281. overrideToCancel.enabled = false
  282. }
  283. }
  284. // Save the context if there are changes
  285. if self.viewContext.hasChanges {
  286. try self.viewContext.save()
  287. // Update the View
  288. self.updateLatestOverrideConfiguration()
  289. }
  290. } catch {
  291. debugPrint(
  292. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
  293. )
  294. }
  295. }
  296. }
  297. // MARK: - Override (presets) save operations
  298. // Saves a Custom Override in a background context
  299. /// not a Preset
  300. func saveCustomOverride() async {
  301. let override = Override(
  302. name: overrideName,
  303. enabled: true,
  304. date: Date(),
  305. duration: overrideDuration,
  306. indefinite: indefinite,
  307. percentage: overridePercentage,
  308. smbIsOff: smbIsOff,
  309. isPreset: isPreset,
  310. id: id,
  311. overrideTarget: shouldOverrideTarget,
  312. target: target,
  313. advancedSettings: advancedSettings,
  314. isfAndCr: isfAndCr,
  315. isf: isf,
  316. cr: cr,
  317. smbIsScheduledOff: smbIsScheduledOff,
  318. start: start,
  319. end: end,
  320. smbMinutes: smbMinutes,
  321. uamMinutes: uamMinutes
  322. )
  323. // First disable all Overrides
  324. await disableAllActiveOverrides(createOverrideRunEntry: true)
  325. // Then save and activate a new custom Override
  326. await overrideStorage.storeOverride(override: override)
  327. // Reset State variables
  328. await resetStateVariables()
  329. // Update View
  330. updateLatestOverrideConfiguration()
  331. }
  332. // Save Presets
  333. /// enabled has to be false, isPreset has to be true
  334. func saveOverridePreset() async {
  335. let preset = Override(
  336. name: overrideName,
  337. enabled: false,
  338. date: Date(),
  339. duration: overrideDuration,
  340. indefinite: indefinite,
  341. percentage: overridePercentage,
  342. smbIsOff: smbIsOff,
  343. isPreset: true,
  344. id: id,
  345. overrideTarget: shouldOverrideTarget,
  346. target: target,
  347. advancedSettings: advancedSettings,
  348. isfAndCr: isfAndCr,
  349. isf: isf,
  350. cr: cr,
  351. smbIsScheduledOff: smbIsScheduledOff,
  352. start: start,
  353. end: end,
  354. smbMinutes: smbMinutes,
  355. uamMinutes: uamMinutes
  356. )
  357. async let storeOverride: () = overrideStorage.storeOverride(override: preset)
  358. async let resetState: () = resetStateVariables()
  359. _ = await (storeOverride, resetState)
  360. // Update Presets View
  361. setupOverridePresetsArray()
  362. await nightscoutManager.uploadProfiles()
  363. }
  364. // MARK: - Setup Override Presets Array
  365. // Fill the array of the Override Presets to display them in the UI
  366. private func setupOverridePresetsArray() {
  367. Task {
  368. let ids = await self.overrideStorage.fetchForOverridePresets()
  369. await updateOverridePresetsArray(with: ids)
  370. }
  371. }
  372. @MainActor private func updateOverridePresetsArray(with IDs: [NSManagedObjectID]) async {
  373. do {
  374. let overrideObjects = try IDs.compactMap { id in
  375. try viewContext.existingObject(with: id) as? OverrideStored
  376. }
  377. overridePresets = overrideObjects
  378. } catch {
  379. debugPrint(
  380. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
  381. )
  382. }
  383. }
  384. // MARK: - Override Preset Deletion
  385. func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
  386. await overrideStorage.deleteOverridePreset(objectID)
  387. // Update Presets View
  388. setupOverridePresetsArray()
  389. await nightscoutManager.uploadProfiles()
  390. }
  391. // MARK: - Setup the State variables with the last Override configuration
  392. /// First get the latest Overrides corresponding NSManagedObjectID with a background fetch
  393. /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
  394. /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
  395. func updateLatestOverrideConfiguration() {
  396. Task {
  397. let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
  398. async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
  399. async let setOverride: () = setCurrentOverride(from: id)
  400. _ = await (updateState, setOverride)
  401. }
  402. }
  403. @MainActor func updateLatestOverrideConfigurationOfState(from IDs: [NSManagedObjectID]) async {
  404. do {
  405. let result = try IDs.compactMap { id in
  406. try viewContext.existingObject(with: id) as? OverrideStored
  407. }
  408. isEnabled = result.first?.enabled ?? false
  409. if !isEnabled {
  410. await resetStateVariables()
  411. }
  412. } catch {
  413. debugPrint(
  414. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to updateLatestOverrideConfiguration"
  415. )
  416. }
  417. }
  418. // Sets the current active Preset name to show in the UI
  419. @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
  420. do {
  421. guard let firstID = IDs.first else {
  422. activeOverrideName = "Custom Override"
  423. currentActiveOverride = nil
  424. return
  425. }
  426. if let overrideToEdit = try viewContext.existingObject(with: firstID) as? OverrideStored {
  427. currentActiveOverride = overrideToEdit
  428. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  429. }
  430. } catch {
  431. debugPrint(
  432. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
  433. )
  434. }
  435. }
  436. @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
  437. // We get the current active Preset by using currentActiveOverride which can either be a Preset or a custom Override
  438. guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset == true else { return }
  439. // Copy the current Override-Preset to not edit the underlying Preset
  440. let duplidateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
  441. // Cancel the duplicated Override
  442. /// As we are on the Main Thread already we don't need to cancel via the objectID in this case
  443. do {
  444. try await viewContext.perform {
  445. overridePresetToDuplicate.enabled = false
  446. guard self.viewContext.hasChanges else { return }
  447. try self.viewContext.save()
  448. }
  449. // Update View
  450. // TODO: -
  451. if let overrideToEdit = try viewContext.existingObject(with: duplidateId) as? OverrideStored
  452. {
  453. currentActiveOverride = overrideToEdit
  454. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  455. }
  456. } catch {
  457. debugPrint(
  458. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
  459. )
  460. }
  461. }
  462. // MARK: - Helper functions for Overrides
  463. @MainActor func resetStateVariables() async {
  464. id = ""
  465. overrideDuration = 0
  466. indefinite = true
  467. overridePercentage = 100
  468. advancedSettings = false
  469. smbIsOff = false
  470. overrideName = ""
  471. shouldOverrideTarget = false
  472. isf = true
  473. cr = true
  474. isfAndCr = true
  475. smbIsScheduledOff = false
  476. start = 0
  477. end = 0
  478. smbMinutes = defaultSmbMinutes
  479. uamMinutes = defaultUamMinutes
  480. target = currentGlucoseTarget
  481. }
  482. static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
  483. // Convert target and step to NSDecimalNumber
  484. guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
  485. let stepValue = NSDecimalNumber(decimal: step).doubleValue as Double?
  486. else {
  487. return target
  488. }
  489. // Perform the remainder check using truncatingRemainder
  490. let remainder = Decimal(targetValue.truncatingRemainder(dividingBy: stepValue))
  491. if remainder != 0 {
  492. // Calculate how much to adjust (up or down) based on the remainder
  493. let adjustment = step - remainder
  494. return target + adjustment
  495. }
  496. // Return the original target if no adjustment is needed
  497. return target
  498. }
  499. static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
  500. let stepDouble = Double(step)
  501. // Check if overridePercentage is not divisible by the selected step
  502. if percentage.truncatingRemainder(dividingBy: stepDouble) != 0 {
  503. let roundedValue: Double
  504. if percentage > 100 {
  505. // Round down to the nearest valid step away from 100
  506. let stepCount = (percentage - 100) / stepDouble
  507. roundedValue = 100 + floor(stepCount) * stepDouble
  508. } else {
  509. // Round up to the nearest valid step away from 100
  510. let stepCount = (100 - percentage) / stepDouble
  511. roundedValue = 100 - floor(stepCount) * stepDouble
  512. }
  513. // Ensure the value stays between 10 and 200
  514. return max(10, min(roundedValue, 200))
  515. }
  516. return percentage
  517. }
  518. }
  519. // MARK: - Temp Targets
  520. extension OverrideConfig.StateModel {
  521. // MARK: - Setup the State variables with the last Temp Target configuration
  522. /// First get the latest Temp Target corresponding NSManagedObjectID with a background fetch
  523. /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
  524. /// This also needs to be called when we cancel an Temp Target via the Home View to update the State of the Button for this case
  525. func updateLatestTempTargetConfiguration() {
  526. Task {
  527. let id = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
  528. async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
  529. async let setTempTarget: () = setCurrentTempTarget(from: id)
  530. _ = await (updateState, setTempTarget)
  531. }
  532. }
  533. @MainActor func updateLatestTempTargetConfigurationOfState(from IDs: [NSManagedObjectID]) async {
  534. do {
  535. let result = try IDs.compactMap { id in
  536. try viewContext.existingObject(with: id) as? TempTargetStored
  537. }
  538. isTempTargetEnabled = result.first?.enabled ?? false
  539. if !isEnabled {
  540. await resetTempTargetState()
  541. }
  542. } catch {
  543. debugPrint(
  544. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest temp target configuration"
  545. )
  546. }
  547. }
  548. // Sets the current active Preset name to show in the UI
  549. @MainActor func setCurrentTempTarget(from IDs: [NSManagedObjectID]) async {
  550. do {
  551. guard let firstID = IDs.first else {
  552. activeTempTargetName = "Custom Temp Target"
  553. currentActiveTempTarget = nil
  554. return
  555. }
  556. if let tempTargetToEdit = try viewContext.existingObject(with: firstID) as? TempTargetStored {
  557. currentActiveTempTarget = tempTargetToEdit
  558. activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
  559. tempTargetTarget = tempTargetToEdit.target?.decimalValue ?? 0
  560. }
  561. } catch {
  562. debugPrint(
  563. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
  564. )
  565. }
  566. }
  567. // Fill the array of the Temp Target Presets to display them in the UI
  568. private func setupTempTargetPresetsArray() {
  569. Task {
  570. let ids = await tempTargetStorage.fetchForTempTargetPresets()
  571. await updateTempTargetPresetsArray(with: ids)
  572. }
  573. }
  574. @MainActor private func updateTempTargetPresetsArray(with IDs: [NSManagedObjectID]) async {
  575. do {
  576. let tempTargetObjects = try IDs.compactMap { id in
  577. try viewContext.existingObject(with: id) as? TempTargetStored
  578. }
  579. tempTargetPresets = tempTargetObjects
  580. } catch {
  581. debugPrint(
  582. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Temp Targets as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
  583. )
  584. }
  585. }
  586. func saveTempTargetToStorage(tempTargets: [TempTarget]) {
  587. tempTargetStorage.saveTempTargetsToStorage(tempTargets)
  588. }
  589. // Creates and enacts a non Preset Temp Target
  590. func saveCustomTempTarget() async {
  591. // First disable all active TempTargets
  592. await disableAllActiveTempTargets(createTempTargetRunEntry: true)
  593. let tempTarget = TempTarget(
  594. name: tempTargetName,
  595. createdAt: Date(),
  596. targetTop: tempTargetTarget,
  597. targetBottom: tempTargetTarget,
  598. duration: tempTargetDuration,
  599. enteredBy: TempTarget.manual,
  600. reason: TempTarget.custom,
  601. isPreset: false,
  602. enabled: true,
  603. halfBasalTarget: halfBasalTarget
  604. )
  605. // Save Temp Target to Core Data
  606. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  607. // Enact Temp Target for oref
  608. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  609. // Reset State variables
  610. await resetTempTargetState()
  611. // Update View
  612. updateLatestTempTargetConfiguration()
  613. }
  614. // Creates a new Temp Target Preset
  615. func saveTempTargetPreset() async {
  616. let tempTarget = TempTarget(
  617. name: tempTargetName,
  618. createdAt: Date(),
  619. targetTop: tempTargetTarget,
  620. targetBottom: tempTargetTarget,
  621. duration: tempTargetDuration,
  622. enteredBy: TempTarget.manual,
  623. reason: TempTarget.custom,
  624. isPreset: true,
  625. enabled: false,
  626. halfBasalTarget: halfBasalTarget
  627. )
  628. // Save to Core Data
  629. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  630. // Reset State variables
  631. await resetTempTargetState()
  632. // Update View
  633. setupTempTargetPresetsArray()
  634. }
  635. // Enact Temp Target Preset
  636. /// here we only have to update the Boolean Flag 'enabled'
  637. @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
  638. do {
  639. /// get the underlying NSManagedObject of the Override that should be enabled
  640. let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
  641. tempTargetToEnact?.enabled = true
  642. tempTargetToEnact?.date = Date()
  643. tempTargetToEnact?.isUploadedToNS = false
  644. /// Update the 'Cancel Temp Target' button state
  645. isTempTargetEnabled = true
  646. /// disable all active Temp Targets and reset state variables
  647. async let disableTempTargets: () = disableAllActiveTempTargets(
  648. except: id,
  649. createTempTargetRunEntry: currentActiveTempTarget != nil
  650. )
  651. async let resetState: () = resetTempTargetState()
  652. _ = await (disableTempTargets, resetState)
  653. if viewContext.hasChanges {
  654. try viewContext.save()
  655. }
  656. // Update View
  657. updateLatestTempTargetConfiguration()
  658. // Map to TempTarget Struct
  659. let tempTarget = TempTarget(
  660. name: tempTargetToEnact?.name,
  661. createdAt: Date(),
  662. targetTop: tempTargetToEnact?.target?.decimalValue,
  663. targetBottom: tempTargetToEnact?.target?.decimalValue,
  664. duration: tempTargetToEnact?.duration?.decimalValue ?? 0,
  665. enteredBy: TempTarget.manual,
  666. reason: TempTarget.custom,
  667. isPreset: true,
  668. enabled: true,
  669. halfBasalTarget: halfBasalTarget
  670. )
  671. // Make sure the Temp Target gets used by Oref
  672. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  673. } catch {
  674. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
  675. }
  676. }
  677. // Disable all active Temp Targets
  678. @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
  679. // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
  680. let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
  681. await viewContext.perform {
  682. do {
  683. // Fetch the existing TempTargetStored objects from the context
  684. let results = try ids.compactMap { id in
  685. try self.viewContext.existingObject(with: id) as? TempTargetStored
  686. }
  687. // If there are no results, return early
  688. guard !results.isEmpty else { return }
  689. // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
  690. if createTempTargetRunEntry {
  691. // Use the first temp target to create a new TempTargetRunStored entry
  692. if let canceledTempTarget = results.first {
  693. let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
  694. newTempTargetRunStored.id = UUID()
  695. newTempTargetRunStored.name = canceledTempTarget.name
  696. newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
  697. newTempTargetRunStored.endDate = Date()
  698. newTempTargetRunStored
  699. .target = canceledTempTarget.target ?? 0
  700. newTempTargetRunStored.tempTarget = canceledTempTarget
  701. newTempTargetRunStored.isUploadedToNS = false
  702. }
  703. }
  704. // Disable all override except the one with overrideID
  705. for tempTargetToCancel in results {
  706. if tempTargetToCancel.objectID != id {
  707. tempTargetToCancel.enabled = false
  708. }
  709. }
  710. // Save the context if there are changes
  711. if self.viewContext.hasChanges {
  712. try self.viewContext.save()
  713. // Update the storage
  714. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
  715. }
  716. } catch {
  717. debugPrint(
  718. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
  719. )
  720. }
  721. }
  722. }
  723. @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
  724. // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
  725. guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
  726. tempTargetPresetToDuplicate.isPreset == true else { return }
  727. // Copy the current TempTarget-Preset to not edit the underlying Preset
  728. let duplidateId = await tempTargetStorage.copyRunningTempTarget(tempTargetPresetToDuplicate)
  729. // Cancel the duplicated Temp Target
  730. /// As we are on the Main Thread already we don't need to cancel via the objectID in this case
  731. do {
  732. try await viewContext.perform {
  733. tempTargetPresetToDuplicate.enabled = false
  734. guard self.viewContext.hasChanges else { return }
  735. try self.viewContext.save()
  736. }
  737. if let tempTargetToEdit = try viewContext.existingObject(with: duplidateId) as? TempTargetStored
  738. {
  739. currentActiveTempTarget = tempTargetToEdit
  740. activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
  741. }
  742. } catch {
  743. debugPrint(
  744. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
  745. )
  746. }
  747. }
  748. // Deletion of Temp Targets
  749. func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
  750. await tempTargetStorage.deleteOverridePreset(objectID)
  751. // Update Presets View
  752. setupTempTargetPresetsArray()
  753. }
  754. @MainActor func resetTempTargetState() async {
  755. tempTargetName = ""
  756. tempTargetTarget = 100
  757. tempTargetDuration = 0
  758. percentage = 100
  759. halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
  760. }
  761. func handleAdjustSensToggle() {
  762. if !didAdjustSens {
  763. halfBasalTarget = settingHalfBasalTarget
  764. percentage = Double(computeAdjustedPercentage(usingHBT: settingHalfBasalTarget) * 100)
  765. }
  766. }
  767. func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
  768. computeSliderHigh(usingTarget: initialTarget) > computeSliderLow(usingTarget: initialTarget)
  769. }
  770. func computeHalfBasalTarget(
  771. usingTarget initialTarget: Decimal? = nil,
  772. usingPercentage initialPercentage: Double? = nil
  773. ) -> Double {
  774. let adjustmentPercentage = initialPercentage ?? percentage
  775. let adjustmentRatio = Decimal(adjustmentPercentage / 100)
  776. let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
  777. var halfBasalTargetValue = halfBasalTarget
  778. if adjustmentRatio != 1 {
  779. halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
  780. (adjustmentRatio - 1)
  781. }
  782. return round(Double(halfBasalTargetValue))
  783. }
  784. func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
  785. let calcTarget = initialTarget ?? tempTargetTarget
  786. guard calcTarget != 0 else { return 15 }
  787. let shouldRaiseSensitivity = settingsManager.preferences.highTemptargetRaisesSensitivity
  788. let isExerciseModeActive = settingsManager.preferences.exerciseMode
  789. let isTargetNormalOrLower = calcTarget <= normalTarget
  790. let minSens = (isTargetNormalOrLower || (!shouldRaiseSensitivity && !isExerciseModeActive)) ? 100 : 15
  791. return Double(max(0, minSens))
  792. }
  793. func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
  794. let calcTarget = initialTarget ?? tempTargetTarget
  795. guard calcTarget != 0 else { return Double(maxValue * 100) }
  796. let shouldLowerSensitivity = settingsManager.preferences.lowTemptargetLowersSensitivity
  797. let isTargetNormalOrHigher = calcTarget >= normalTarget
  798. let maxSens = (isTargetNormalOrHigher || !shouldLowerSensitivity) ? 100 : Double(maxValue * 100)
  799. return maxSens
  800. }
  801. func computeAdjustedPercentage(
  802. usingHBT initialHalfBasalTarget: Decimal? = nil,
  803. usingTarget initialTarget: Decimal? = nil
  804. ) -> Decimal {
  805. let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
  806. let calcTarget = initialTarget ?? tempTargetTarget
  807. let deviationFromNormal = halfBasalTargetValue - normalTarget
  808. let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
  809. let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
  810. adjustmentFactor
  811. return min(adjustmentRatio, maxValue)
  812. }
  813. }
  814. extension OverrideConfig.StateModel: SettingsObserver {
  815. func settingsDidChange(_: FreeAPSSettings) {
  816. units = settingsManager.settings.units
  817. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  818. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  819. maxValue = settingsManager.preferences.autosensMax
  820. minValue = settingsManager.preferences.autosensMin
  821. Task {
  822. await getCurrentGlucoseTarget()
  823. }
  824. }
  825. }
  826. extension PickerSettingsProvider {
  827. func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits, roundMinToStep: Bool) -> [Decimal] {
  828. if !roundMinToStep {
  829. return generatePickerValues(from: setting, units: units)
  830. }
  831. // Adjust min to be divisible by step
  832. var newSetting = setting
  833. var min = Double(newSetting.min)
  834. let step = Double(newSetting.step)
  835. let remainder = min.truncatingRemainder(dividingBy: step)
  836. if remainder != 0 {
  837. // Move min up to the next value divisible by targetStep
  838. min += (step - remainder)
  839. }
  840. newSetting.min = Decimal(min)
  841. return generatePickerValues(from: newSetting, units: units)
  842. }
  843. }
  844. enum TempTargetSensitivityAdjustmentType: String, CaseIterable {
  845. case standard = "Standard"
  846. case slider = "Customized"
  847. }
  848. enum IsfAndOrCrOptions: String, CaseIterable {
  849. case isfAndCr = "ISF/CR"
  850. case isf = "ISF"
  851. case cr = "CR"
  852. case nothing = "None"
  853. }
  854. enum DisableSmbOptions: String, CaseIterable {
  855. case dontDisable = "Don't Disable"
  856. case disable = "Disable"
  857. case disableOnSchedule = "Disable on Schedule"
  858. }
  859. func percentageDescription(_ percent: Double) -> Text? {
  860. if percent.isNaN || percent == 100 { return nil }
  861. var description: String = "Insulin doses will be "
  862. if percent < 100 {
  863. description += "decreased by "
  864. } else {
  865. description += "increased by "
  866. }
  867. let deviationFrom100 = abs(percent - 100)
  868. description += String(format: "%.0f% %.", deviationFrom100)
  869. return Text(description)
  870. }
  871. // Function to check if the phone is using 24-hour format
  872. func is24HourFormat() -> Bool {
  873. let formatter = DateFormatter()
  874. formatter.locale = Locale.current
  875. formatter.dateStyle = .none
  876. formatter.timeStyle = .short
  877. let dateString = formatter.string(from: Date())
  878. return !dateString.contains("AM") && !dateString.contains("PM")
  879. }
  880. // Helper function to convert hours to AM/PM format
  881. func convertTo12HourFormat(_ hour: Int) -> String {
  882. let formatter = DateFormatter()
  883. formatter.dateFormat = "h a"
  884. // Create a date from the hour and format it to AM/PM
  885. let calendar = Calendar.current
  886. let components = DateComponents(hour: hour)
  887. let date = calendar.date(from: components) ?? Date()
  888. return formatter.string(from: date)
  889. }
  890. // Helper function to format 24-hour numbers as two digits
  891. func format24Hour(_ hour: Int) -> String {
  892. String(format: "%02d", hour)
  893. }
  894. func formatHrMin(_ durationInMinutes: Int) -> String {
  895. let hours = durationInMinutes / 60
  896. let minutes = durationInMinutes % 60
  897. switch (hours, minutes) {
  898. case let (0, m):
  899. return "\(m) min"
  900. case let (h, 0):
  901. return "\(h) hr"
  902. default:
  903. return "\(hours) hr \(minutes) min"
  904. }
  905. }
  906. func convertToMinutes(_ hours: Int, _ minutes: Int) -> Decimal {
  907. let totalMinutes = (hours * 60) + minutes
  908. return Decimal(max(0, totalMinutes))
  909. }
  910. struct RadioButton: View {
  911. var isSelected: Bool
  912. var label: String
  913. var action: () -> Void
  914. var body: some View {
  915. Button(action: {
  916. action()
  917. }) {
  918. HStack {
  919. Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
  920. Text(label) // Add label inside the button to make it tappable
  921. }
  922. }
  923. .buttonStyle(PlainButtonStyle())
  924. }
  925. }