AdjustmentsStateModel+Overrides.swift 15 KB

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