AdjustmentsStateModel+Overrides.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. isOverrideEnabled = 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 { [weak self] in
  166. guard let self = self else { return }
  167. let id = await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
  168. // execute sequentially instead of concurrently
  169. await self.updateLatestOverrideConfigurationOfState(from: id)
  170. await self.setCurrentOverride(from: id)
  171. }
  172. }
  173. /// Updates state variables with the latest Override configuration.
  174. @MainActor func updateLatestOverrideConfigurationOfState(from IDs: [NSManagedObjectID]) async {
  175. do {
  176. let result = try IDs.compactMap { id in
  177. try viewContext.existingObject(with: id) as? OverrideStored
  178. }
  179. isOverrideEnabled = result.first?.enabled ?? false
  180. if !isOverrideEnabled {
  181. await resetStateVariables()
  182. }
  183. } catch {
  184. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest Override configuration")
  185. }
  186. }
  187. /// Sets the current active Override for UI purposes.
  188. @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
  189. do {
  190. guard let firstID = IDs.first else {
  191. activeOverrideName = "Custom Override"
  192. currentActiveOverride = nil
  193. return
  194. }
  195. if let overrideToEdit = try viewContext.existingObject(with: firstID) as? OverrideStored {
  196. currentActiveOverride = overrideToEdit
  197. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  198. }
  199. } catch {
  200. debugPrint(
  201. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active Override: \(error.localizedDescription)"
  202. )
  203. }
  204. }
  205. /// Duplicates the active Override Preset and cancels the previous one.
  206. @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
  207. guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset else { return }
  208. let duplicateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
  209. do {
  210. try await viewContext.perform {
  211. overridePresetToDuplicate.enabled = false
  212. guard self.viewContext.hasChanges else { return }
  213. try self.viewContext.save()
  214. }
  215. if let overrideToEdit = try viewContext.existingObject(with: duplicateId) as? OverrideStored {
  216. currentActiveOverride = overrideToEdit
  217. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  218. }
  219. } catch {
  220. debugPrint(
  221. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous Override: \(error.localizedDescription)"
  222. )
  223. }
  224. }
  225. // MARK: - Helper Functions
  226. /// Resets state variables to default values.
  227. @MainActor func resetStateVariables() async {
  228. id = ""
  229. overrideDuration = 0
  230. indefinite = true
  231. overridePercentage = 100
  232. advancedSettings = false
  233. smbIsOff = false
  234. overrideName = ""
  235. shouldOverrideTarget = false
  236. isf = true
  237. cr = true
  238. isfAndCr = true
  239. smbIsScheduledOff = false
  240. start = 0
  241. end = 0
  242. smbMinutes = defaultSmbMinutes
  243. uamMinutes = defaultUamMinutes
  244. target = currentGlucoseTarget
  245. }
  246. /// Rounds a target value to the nearest step.
  247. static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
  248. // Convert target and step to NSDecimalNumber
  249. guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
  250. let stepValue = NSDecimalNumber(decimal: step).doubleValue as Double?
  251. else {
  252. return target
  253. }
  254. // Perform the remainder check using truncatingRemainder
  255. let remainder = Decimal(targetValue.truncatingRemainder(dividingBy: stepValue))
  256. if remainder != 0 {
  257. // Calculate how much to adjust (up or down) based on the remainder
  258. let adjustment = step - remainder
  259. return target + adjustment
  260. }
  261. // Return the original target if no adjustment is needed
  262. return target
  263. }
  264. /// Rounds an Override percentage to the nearest step.
  265. static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
  266. let stepDouble = Double(step)
  267. // Check if overridePercentage is not divisible by the selected step
  268. if percentage.truncatingRemainder(dividingBy: stepDouble) != 0 {
  269. let roundedValue: Double
  270. if percentage > 100 {
  271. // Round down to the nearest valid step away from 100
  272. let stepCount = (percentage - 100) / stepDouble
  273. roundedValue = 100 + floor(stepCount) * stepDouble
  274. } else {
  275. // Round up to the nearest valid step away from 100
  276. let stepCount = (100 - percentage) / stepDouble
  277. roundedValue = 100 - floor(stepCount) * stepDouble
  278. }
  279. // Ensure the value stays between 10 and 200
  280. return max(10, min(roundedValue, 200))
  281. }
  282. return percentage
  283. }
  284. }
  285. enum IsfAndOrCrOptions: String, CaseIterable {
  286. case isfAndCr = "ISF/CR"
  287. case isf = "ISF"
  288. case cr = "CR"
  289. case nothing = "None"
  290. }
  291. enum DisableSmbOptions: String, CaseIterable {
  292. case dontDisable = "Don't Disable"
  293. case disable = "Disable"
  294. case disableOnSchedule = "Disable on Schedule"
  295. }