AdjustmentsStateModel+Overrides.swift 14 KB

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