AdjustmentsStateModel+TempTargets.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. extension Adjustments.StateModel {
  5. // MARK: - State Initialization and Updates
  6. /// Updates the latest Temp Target configuration for UI state and logic.
  7. /// First get the latest Temp Target corresponding NSManagedObjectID with a background fetch
  8. /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
  9. /// 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
  10. func updateLatestTempTargetConfiguration() {
  11. Task {
  12. let id = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
  13. async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
  14. async let setTempTarget: () = setCurrentTempTarget(from: id)
  15. _ = await (updateState, setTempTarget)
  16. }
  17. }
  18. /// Updates state variables with the latest Temp Target configuration.
  19. @MainActor func updateLatestTempTargetConfigurationOfState(from IDs: [NSManagedObjectID]) async {
  20. do {
  21. let result = try IDs.compactMap { id in
  22. try viewContext.existingObject(with: id) as? TempTargetStored
  23. }
  24. isTempTargetEnabled = result.first?.enabled ?? false
  25. if !isEnabled {
  26. await resetTempTargetState()
  27. }
  28. } catch {
  29. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest temp target configuration")
  30. }
  31. }
  32. /// Sets the current Temp Target for UI and logic purposes.
  33. @MainActor func setCurrentTempTarget(from IDs: [NSManagedObjectID]) async {
  34. do {
  35. guard let firstID = IDs.first else {
  36. activeTempTargetName = "Custom Temp Target"
  37. currentActiveTempTarget = nil
  38. return
  39. }
  40. if let tempTargetToEdit = try viewContext.existingObject(with: firstID) as? TempTargetStored {
  41. currentActiveTempTarget = tempTargetToEdit
  42. activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
  43. tempTargetTarget = tempTargetToEdit.target?.decimalValue ?? 0
  44. }
  45. } catch {
  46. debugPrint(
  47. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
  48. )
  49. }
  50. }
  51. // MARK: - Temp Target Fetching and Setup
  52. /// Sets up Temp Targets using fetch and update functions.
  53. func setupTempTargets(
  54. fetchFunction: @escaping () async -> [NSManagedObjectID],
  55. updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
  56. ) {
  57. Task {
  58. let ids = await fetchFunction()
  59. let tempTargetObjects = await fetchTempTargetObjects(for: ids)
  60. await updateFunction(tempTargetObjects)
  61. }
  62. }
  63. /// Fetches Temp Target objects from Core Data.
  64. @MainActor private func fetchTempTargetObjects(for IDs: [NSManagedObjectID]) async -> [TempTargetStored] {
  65. do {
  66. return try IDs.compactMap { id in
  67. try viewContext.existingObject(with: id) as? TempTargetStored
  68. }
  69. } catch {
  70. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Targets")
  71. return []
  72. }
  73. }
  74. /// Sets up the Temp Target presets array for the view.
  75. func setupTempTargetPresetsArray() {
  76. setupTempTargets(
  77. fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
  78. updateFunction: { tempTargets in
  79. self.tempTargetPresets = tempTargets
  80. }
  81. )
  82. }
  83. /// Sets up the scheduled Temp Targets array for the view.
  84. func setupScheduledTempTargetsArray() {
  85. setupTempTargets(
  86. fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
  87. updateFunction: { tempTargets in
  88. self.scheduledTempTargets = tempTargets
  89. }
  90. )
  91. }
  92. // MARK: - Temp Target Creation and Management
  93. /// Saves a Temp Target to storage.
  94. func saveTempTargetToStorage(tempTargets: [TempTarget]) {
  95. tempTargetStorage.saveTempTargetsToStorage(tempTargets)
  96. }
  97. /// Saves a Temp Target based on whether it is scheduled or custom.
  98. func invokeSaveOfCustomTempTargets() async {
  99. if date > Date() {
  100. await saveScheduledTempTarget()
  101. } else {
  102. await saveCustomTempTarget()
  103. }
  104. }
  105. /// Saves a scheduled Temp Target and activates it at the specified date.
  106. func saveScheduledTempTarget() async {
  107. let date = self.date
  108. guard date > Date() else { return }
  109. let tempTarget = TempTarget(
  110. name: tempTargetName,
  111. createdAt: date,
  112. targetTop: tempTargetTarget,
  113. targetBottom: tempTargetTarget,
  114. duration: tempTargetDuration,
  115. enteredBy: TempTarget.local,
  116. reason: TempTarget.custom,
  117. isPreset: false,
  118. enabled: false,
  119. halfBasalTarget: halfBasalTarget
  120. )
  121. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  122. setupScheduledTempTargetsArray()
  123. Task {
  124. await waitUntilDate(date)
  125. await disableAllActiveTempTargets(createTempTargetRunEntry: true)
  126. await enableScheduledTempTarget(for: date)
  127. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  128. }
  129. }
  130. /// Enables a scheduled Temp Target for a specific date.
  131. func enableScheduledTempTarget(for date: Date) async {
  132. let ids = await tempTargetStorage.fetchScheduledTempTarget(for: date)
  133. guard let firstID = ids.first else {
  134. debugPrint("No Temp Target found for the specified date.")
  135. return
  136. }
  137. await setCurrentTempTarget(from: ids)
  138. await MainActor.run {
  139. do {
  140. if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
  141. tempTarget.enabled = true
  142. try viewContext.save()
  143. isTempTargetEnabled = true
  144. }
  145. } catch {
  146. debugPrint("Failed to enable the Temp Target: \(error.localizedDescription)")
  147. }
  148. }
  149. setupScheduledTempTargetsArray()
  150. }
  151. /// Waits until a target date before proceeding.
  152. private func waitUntilDate(_ targetDate: Date) async {
  153. while Date() < targetDate {
  154. let timeInterval = targetDate.timeIntervalSince(Date())
  155. let sleepDuration = min(timeInterval, 60.0)
  156. try? await Task.sleep(nanoseconds: UInt64(sleepDuration * 1_000_000_000))
  157. }
  158. }
  159. /// Saves a custom Temp Target and disables existing ones.
  160. func saveCustomTempTarget() async {
  161. await disableAllActiveTempTargets(createTempTargetRunEntry: true)
  162. let tempTarget = TempTarget(
  163. name: tempTargetName,
  164. /// We don't need to use the state var date here as we are using a different function for scheduled Temp Targets 'saveScheduledTempTarget()'
  165. createdAt: Date(),
  166. targetTop: tempTargetTarget,
  167. targetBottom: tempTargetTarget,
  168. duration: tempTargetDuration,
  169. enteredBy: TempTarget.local,
  170. reason: TempTarget.custom,
  171. isPreset: false,
  172. enabled: true,
  173. halfBasalTarget: halfBasalTarget
  174. )
  175. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  176. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  177. await resetTempTargetState()
  178. isTempTargetEnabled = true
  179. updateLatestTempTargetConfiguration()
  180. }
  181. /// Creates a new Temp Target preset.
  182. func saveTempTargetPreset() async {
  183. let tempTarget = TempTarget(
  184. name: tempTargetName,
  185. createdAt: Date(),
  186. targetTop: tempTargetTarget,
  187. targetBottom: tempTargetTarget,
  188. duration: tempTargetDuration,
  189. enteredBy: TempTarget.local,
  190. reason: TempTarget.custom,
  191. isPreset: true,
  192. enabled: false,
  193. halfBasalTarget: halfBasalTarget
  194. )
  195. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  196. await resetTempTargetState()
  197. setupTempTargetPresetsArray()
  198. }
  199. /// Enacts a Temp Target preset by enabling it.
  200. @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
  201. do {
  202. let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
  203. tempTargetToEnact?.enabled = true
  204. tempTargetToEnact?.date = Date()
  205. tempTargetToEnact?.isUploadedToNS = false
  206. isTempTargetEnabled = true
  207. async let disableTempTargets: () = disableAllActiveTempTargets(
  208. except: id,
  209. createTempTargetRunEntry: currentActiveTempTarget != nil
  210. )
  211. async let resetState: () = resetTempTargetState()
  212. _ = await (disableTempTargets, resetState)
  213. if viewContext.hasChanges {
  214. try viewContext.save()
  215. }
  216. updateLatestTempTargetConfiguration()
  217. let tempTarget = TempTarget(
  218. name: tempTargetToEnact?.name,
  219. createdAt: Date(),
  220. targetTop: tempTargetToEnact?.target?.decimalValue,
  221. targetBottom: tempTargetToEnact?.target?.decimalValue,
  222. duration: tempTargetToEnact?.duration?.decimalValue ?? 0,
  223. enteredBy: TempTarget.local,
  224. reason: TempTarget.custom,
  225. isPreset: true,
  226. enabled: true,
  227. halfBasalTarget: halfBasalTarget
  228. )
  229. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  230. } catch {
  231. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
  232. }
  233. }
  234. /// Disables all active Temp Targets.
  235. @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
  236. // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
  237. let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
  238. await viewContext.perform {
  239. do {
  240. // Fetch the existing TempTargetStored objects from the context
  241. let results = try ids.compactMap { id in
  242. try self.viewContext.existingObject(with: id) as? TempTargetStored
  243. }
  244. // If there are no results, return early
  245. guard !results.isEmpty else { return }
  246. // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
  247. if createTempTargetRunEntry {
  248. // Use the first temp target to create a new TempTargetRunStored entry
  249. if let canceledTempTarget = results.first {
  250. let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
  251. newTempTargetRunStored.id = UUID()
  252. newTempTargetRunStored.name = canceledTempTarget.name
  253. newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
  254. newTempTargetRunStored.endDate = Date()
  255. newTempTargetRunStored
  256. .target = canceledTempTarget.target ?? 0
  257. newTempTargetRunStored.tempTarget = canceledTempTarget
  258. newTempTargetRunStored.isUploadedToNS = false
  259. }
  260. }
  261. // Disable all temporary targets except the one with given id
  262. for tempTargetToCancel in results {
  263. if tempTargetToCancel.objectID != id {
  264. tempTargetToCancel.enabled = false
  265. }
  266. }
  267. // Save the context if there are changes
  268. if self.viewContext.hasChanges {
  269. try self.viewContext.save()
  270. // Update the storage
  271. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
  272. }
  273. } catch {
  274. debugPrint(
  275. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
  276. )
  277. }
  278. }
  279. }
  280. /// Duplicates the current preset and cancels the previous one.
  281. @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
  282. // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
  283. guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
  284. tempTargetPresetToDuplicate.isPreset == true else { return }
  285. // Copy the current TempTarget-Preset to not edit the underlying Preset
  286. let duplidateId = await tempTargetStorage.copyRunningTempTarget(tempTargetPresetToDuplicate)
  287. // Cancel the duplicated Temp Target
  288. // As we are on the Main Thread already we don't need to cancel via the objectID in this case
  289. do {
  290. try await viewContext.perform {
  291. tempTargetPresetToDuplicate.enabled = false
  292. guard self.viewContext.hasChanges else { return }
  293. try self.viewContext.save()
  294. }
  295. if let tempTargetToEdit = try viewContext.existingObject(with: duplidateId) as? TempTargetStored
  296. {
  297. currentActiveTempTarget = tempTargetToEdit
  298. activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
  299. }
  300. } catch {
  301. debugPrint(
  302. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
  303. )
  304. }
  305. }
  306. /// Deletes a Temp Target preset.
  307. func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
  308. await tempTargetStorage.deleteOverridePreset(objectID)
  309. setupTempTargetPresetsArray()
  310. }
  311. /// Resets Temp Target state variables.
  312. @MainActor func resetTempTargetState() async {
  313. tempTargetName = ""
  314. tempTargetTarget = 100
  315. tempTargetDuration = 0
  316. percentage = 100
  317. halfBasalTarget = settingHalfBasalTarget
  318. }
  319. // MARK: - Calculations
  320. /// Computes the half-basal target based on the current settings.
  321. func computeHalfBasalTarget(
  322. usingTarget initialTarget: Decimal? = nil,
  323. usingPercentage initialPercentage: Double? = nil
  324. ) -> Double {
  325. let adjustmentPercentage = initialPercentage ?? percentage
  326. let adjustmentRatio = Decimal(adjustmentPercentage / 100)
  327. let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
  328. var halfBasalTargetValue = halfBasalTarget
  329. if adjustmentRatio != 1 {
  330. halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
  331. (adjustmentRatio - 1)
  332. }
  333. return round(Double(halfBasalTargetValue))
  334. }
  335. /// Determines if sensitivity adjustment is enabled based on target.
  336. func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
  337. let target = initialTarget ?? tempTargetTarget
  338. if target < normalTarget, lowTTlowersSens { return true }
  339. if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
  340. return false
  341. }
  342. /// Computes the low value for the slider based on the target.
  343. func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
  344. let calcTarget = initialTarget ?? tempTargetTarget
  345. guard calcTarget != 0 else { return 15 } // oref defined maximum sensitivity
  346. let minSens = calcTarget < normalTarget ? 105 : 15
  347. return Double(max(0, minSens))
  348. }
  349. /// Computes the high value for the slider based on the target.
  350. func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
  351. let calcTarget = initialTarget ?? tempTargetTarget
  352. guard calcTarget != 0 else { return Double(maxValue * 100) } // oref defined limit for increased insulin delivery
  353. let maxSens = calcTarget > normalTarget ? 95 : Double(maxValue * 100)
  354. return maxSens
  355. }
  356. /// Computes the adjusted percentage for the slider.
  357. func computeAdjustedPercentage(
  358. usingHBT initialHalfBasalTarget: Decimal? = nil,
  359. usingTarget initialTarget: Decimal? = nil
  360. ) -> Double {
  361. let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
  362. let calcTarget = initialTarget ?? tempTargetTarget
  363. let deviationFromNormal = halfBasalTargetValue - normalTarget
  364. let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
  365. let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
  366. adjustmentFactor
  367. return Double(min(adjustmentRatio, maxValue) * 100).rounded()
  368. }
  369. }
  370. enum TempTargetSensitivityAdjustmentType: String, CaseIterable {
  371. case standard = "Standard"
  372. case slider = "Custom"
  373. }