AdjustmentsStateModel+Overrides.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. extension Adjustments.StateModel {
  5. // MARK: - Enact Overrides
  6. /// Enacts an Override Preset by enabling it and disabling others.
  7. @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
  8. do {
  9. let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
  10. overrideToEnact?.enabled = true
  11. overrideToEnact?.date = Date()
  12. overrideToEnact?.isUploadedToNS = false
  13. isEnabled = true
  14. await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
  15. await resetStateVariables()
  16. guard viewContext.hasChanges else { return }
  17. try viewContext.save()
  18. updateLatestOverrideConfiguration()
  19. } catch {
  20. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
  21. }
  22. }
  23. // MARK: - Disable Overrides
  24. /// Disables all active Overrides, optionally creating a run entry.
  25. @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
  26. // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
  27. let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
  28. await viewContext.perform {
  29. do {
  30. // Fetch the existing OverrideStored objects from the context
  31. let results = try ids.compactMap { id in
  32. try self.viewContext.existingObject(with: id) as? OverrideStored
  33. }
  34. guard !results.isEmpty else { return }
  35. // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
  36. if createOverrideRunEntry {
  37. // Use the first override to create a new OverrideRunStored entry
  38. if let canceledOverride = results.first {
  39. let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
  40. newOverrideRunStored.id = UUID()
  41. newOverrideRunStored.name = canceledOverride.name
  42. newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
  43. newOverrideRunStored.endDate = Date()
  44. newOverrideRunStored
  45. .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
  46. newOverrideRunStored.override = canceledOverride
  47. newOverrideRunStored.isUploadedToNS = false
  48. }
  49. }
  50. // Disable all overrides except the one with overrideID
  51. for overrideToCancel in results where overrideToCancel.objectID != overrideID {
  52. overrideToCancel.enabled = false
  53. }
  54. if self.viewContext.hasChanges {
  55. // Save changes and update the View
  56. try self.viewContext.save()
  57. self.updateLatestOverrideConfiguration()
  58. }
  59. } catch {
  60. debugPrint(
  61. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides: \(error.localizedDescription)"
  62. )
  63. }
  64. }
  65. }
  66. // MARK: - Save Overrides
  67. /// Saves a custom Override and activates it.
  68. func saveCustomOverride() async {
  69. let override = Override(
  70. name: overrideName,
  71. enabled: true,
  72. date: Date(),
  73. duration: overrideDuration,
  74. indefinite: indefinite,
  75. percentage: overridePercentage,
  76. smbIsOff: smbIsOff,
  77. isPreset: isPreset,
  78. id: id,
  79. overrideTarget: shouldOverrideTarget,
  80. target: target,
  81. advancedSettings: advancedSettings,
  82. isfAndCr: isfAndCr,
  83. isf: isf,
  84. cr: cr,
  85. smbIsScheduledOff: smbIsScheduledOff,
  86. start: start,
  87. end: end,
  88. smbMinutes: smbMinutes,
  89. uamMinutes: uamMinutes
  90. )
  91. // First disable all Overrides
  92. await disableAllActiveOverrides(createOverrideRunEntry: true)
  93. // Then save and activate a new custom Override
  94. await overrideStorage.storeOverride(override: override)
  95. // Reset State variables
  96. await resetStateVariables()
  97. // Update View
  98. updateLatestOverrideConfiguration()
  99. }
  100. /// Saves an Override Preset without activating it.
  101. /// `enabled` has to be false
  102. /// `isPreset` has to be true
  103. func saveOverridePreset() async {
  104. let preset = Override(
  105. name: overrideName,
  106. enabled: false,
  107. date: Date(),
  108. duration: overrideDuration,
  109. indefinite: indefinite,
  110. percentage: overridePercentage,
  111. smbIsOff: smbIsOff,
  112. isPreset: true,
  113. id: id,
  114. overrideTarget: shouldOverrideTarget,
  115. target: target,
  116. advancedSettings: advancedSettings,
  117. isfAndCr: isfAndCr,
  118. isf: isf,
  119. cr: cr,
  120. smbIsScheduledOff: smbIsScheduledOff,
  121. start: start,
  122. end: end,
  123. smbMinutes: smbMinutes,
  124. uamMinutes: uamMinutes
  125. )
  126. async let storeOverride: () = overrideStorage.storeOverride(override: preset)
  127. async let resetState: () = resetStateVariables()
  128. _ = await (storeOverride, resetState)
  129. setupOverridePresetsArray()
  130. await nightscoutManager.uploadProfiles()
  131. }
  132. // MARK: - Override Preset Management
  133. /// Sets up the array of Override Presets for UI display.
  134. func setupOverridePresetsArray() {
  135. Task {
  136. let ids = await overrideStorage.fetchForOverridePresets()
  137. await updateOverridePresetsArray(with: ids)
  138. }
  139. }
  140. /// Updates the array of Override Presets from Core Data.
  141. @MainActor private func updateOverridePresetsArray(with IDs: [NSManagedObjectID]) async {
  142. do {
  143. let overrideObjects = try IDs.compactMap { id in
  144. try viewContext.existingObject(with: id) as? OverrideStored
  145. }
  146. overridePresets = overrideObjects
  147. } catch {
  148. debugPrint(
  149. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides: \(error.localizedDescription)"
  150. )
  151. }
  152. }
  153. /// Deletes an Override Preset and updates the view.
  154. func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
  155. await overrideStorage.deleteOverridePreset(objectID)
  156. setupOverridePresetsArray()
  157. await nightscoutManager.uploadProfiles()
  158. }
  159. // MARK: - Update Latest Override Configuration
  160. /// Updates the latest Override configuration and state.
  161. /// First get the latest Overrides corresponding NSManagedObjectID with a background fetch
  162. /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
  163. /// 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
  164. func updateLatestOverrideConfiguration() {
  165. Task {
  166. let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
  167. async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
  168. async let setOverride: () = setCurrentOverride(from: id)
  169. _ = await (updateState, setOverride)
  170. }
  171. }
  172. /// Updates state variables with the latest Override configuration.
  173. @MainActor func updateLatestOverrideConfigurationOfState(from IDs: [NSManagedObjectID]) async {
  174. do {
  175. let result = try IDs.compactMap { id in
  176. try viewContext.existingObject(with: id) as? OverrideStored
  177. }
  178. isEnabled = result.first?.enabled ?? false
  179. if !isEnabled {
  180. await resetStateVariables()
  181. }
  182. } catch {
  183. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest Override configuration")
  184. }
  185. }
  186. /// Sets the current active Override for UI purposes.
  187. @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
  188. do {
  189. guard let firstID = IDs.first else {
  190. activeOverrideName = "Custom Override"
  191. currentActiveOverride = nil
  192. return
  193. }
  194. if let overrideToEdit = try viewContext.existingObject(with: firstID) as? OverrideStored {
  195. currentActiveOverride = overrideToEdit
  196. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  197. }
  198. } catch {
  199. debugPrint(
  200. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active Override: \(error.localizedDescription)"
  201. )
  202. }
  203. }
  204. /// Duplicates the active Override Preset and cancels the previous one.
  205. @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
  206. guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset else { return }
  207. let duplicateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
  208. do {
  209. try await viewContext.perform {
  210. overridePresetToDuplicate.enabled = false
  211. guard self.viewContext.hasChanges else { return }
  212. try self.viewContext.save()
  213. }
  214. if let overrideToEdit = try viewContext.existingObject(with: duplicateId) as? OverrideStored {
  215. currentActiveOverride = overrideToEdit
  216. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  217. }
  218. } catch {
  219. debugPrint(
  220. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous Override: \(error.localizedDescription)"
  221. )
  222. }
  223. }
  224. // MARK: - Helper Functions
  225. /// Resets state variables to default values.
  226. @MainActor func resetStateVariables() async {
  227. id = ""
  228. overrideDuration = 0
  229. indefinite = true
  230. overridePercentage = 100
  231. advancedSettings = false
  232. smbIsOff = false
  233. overrideName = ""
  234. shouldOverrideTarget = false
  235. isf = true
  236. cr = true
  237. isfAndCr = true
  238. smbIsScheduledOff = false
  239. start = 0
  240. end = 0
  241. smbMinutes = defaultSmbMinutes
  242. uamMinutes = defaultUamMinutes
  243. target = currentGlucoseTarget
  244. }
  245. /// Rounds a target value to the nearest step.
  246. static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
  247. // Convert target and step to NSDecimalNumber
  248. guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
  249. let stepValue = NSDecimalNumber(decimal: step).doubleValue as Double?
  250. else {
  251. return target
  252. }
  253. // Perform the remainder check using truncatingRemainder
  254. let remainder = Decimal(targetValue.truncatingRemainder(dividingBy: stepValue))
  255. if remainder != 0 {
  256. // Calculate how much to adjust (up or down) based on the remainder
  257. let adjustment = step - remainder
  258. return target + adjustment
  259. }
  260. // Return the original target if no adjustment is needed
  261. return target
  262. }
  263. /// Rounds an Override percentage to the nearest step.
  264. static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
  265. let stepDouble = Double(step)
  266. // Check if overridePercentage is not divisible by the selected step
  267. if percentage.truncatingRemainder(dividingBy: stepDouble) != 0 {
  268. let roundedValue: Double
  269. if percentage > 100 {
  270. // Round down to the nearest valid step away from 100
  271. let stepCount = (percentage - 100) / stepDouble
  272. roundedValue = 100 + floor(stepCount) * stepDouble
  273. } else {
  274. // Round up to the nearest valid step away from 100
  275. let stepCount = (100 - percentage) / stepDouble
  276. roundedValue = 100 - floor(stepCount) * stepDouble
  277. }
  278. // Ensure the value stays between 10 and 200
  279. return max(10, min(roundedValue, 200))
  280. }
  281. return percentage
  282. }
  283. }
  284. enum IsfAndOrCrOptions: String, CaseIterable {
  285. case isfAndCr = "ISF/CR"
  286. case isf = "ISF"
  287. case cr = "CR"
  288. case nothing = "None"
  289. }
  290. enum DisableSmbOptions: String, CaseIterable {
  291. case dontDisable = "Don't Disable"
  292. case disable = "Disable"
  293. case disableOnSchedule = "Disable on Schedule"
  294. }