AdjustmentsStateModel+TempTargets.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. createdAt: date,
  165. targetTop: tempTargetTarget,
  166. targetBottom: tempTargetTarget,
  167. duration: tempTargetDuration,
  168. enteredBy: TempTarget.local,
  169. reason: TempTarget.custom,
  170. isPreset: false,
  171. enabled: true,
  172. halfBasalTarget: halfBasalTarget
  173. )
  174. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  175. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  176. await resetTempTargetState()
  177. isTempTargetEnabled = true
  178. updateLatestTempTargetConfiguration()
  179. }
  180. /// Creates a new Temp Target preset.
  181. func saveTempTargetPreset() async {
  182. let tempTarget = TempTarget(
  183. name: tempTargetName,
  184. createdAt: Date(),
  185. targetTop: tempTargetTarget,
  186. targetBottom: tempTargetTarget,
  187. duration: tempTargetDuration,
  188. enteredBy: TempTarget.local,
  189. reason: TempTarget.custom,
  190. isPreset: true,
  191. enabled: false,
  192. halfBasalTarget: halfBasalTarget
  193. )
  194. await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
  195. await resetTempTargetState()
  196. setupTempTargetPresetsArray()
  197. }
  198. /// Enacts a Temp Target preset by enabling it.
  199. @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
  200. do {
  201. let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
  202. tempTargetToEnact?.enabled = true
  203. tempTargetToEnact?.date = Date()
  204. tempTargetToEnact?.isUploadedToNS = false
  205. isTempTargetEnabled = true
  206. async let disableTempTargets: () = disableAllActiveTempTargets(
  207. except: id,
  208. createTempTargetRunEntry: currentActiveTempTarget != nil
  209. )
  210. async let resetState: () = resetTempTargetState()
  211. _ = await (disableTempTargets, resetState)
  212. if viewContext.hasChanges {
  213. try viewContext.save()
  214. }
  215. updateLatestTempTargetConfiguration()
  216. let tempTarget = TempTarget(
  217. name: tempTargetToEnact?.name,
  218. createdAt: Date(),
  219. targetTop: tempTargetToEnact?.target?.decimalValue,
  220. targetBottom: tempTargetToEnact?.target?.decimalValue,
  221. duration: tempTargetToEnact?.duration?.decimalValue ?? 0,
  222. enteredBy: TempTarget.local,
  223. reason: TempTarget.custom,
  224. isPreset: true,
  225. enabled: true,
  226. halfBasalTarget: halfBasalTarget
  227. )
  228. tempTargetStorage.saveTempTargetsToStorage([tempTarget])
  229. } catch {
  230. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
  231. }
  232. }
  233. /// Disables all active Temp Targets.
  234. @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
  235. // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
  236. let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
  237. await viewContext.perform {
  238. do {
  239. // Fetch the existing TempTargetStored objects from the context
  240. let results = try ids.compactMap { id in
  241. try self.viewContext.existingObject(with: id) as? TempTargetStored
  242. }
  243. // If there are no results, return early
  244. guard !results.isEmpty else { return }
  245. // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
  246. if createTempTargetRunEntry {
  247. // Use the first temp target to create a new TempTargetRunStored entry
  248. if let canceledTempTarget = results.first {
  249. let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
  250. newTempTargetRunStored.id = UUID()
  251. newTempTargetRunStored.name = canceledTempTarget.name
  252. newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
  253. newTempTargetRunStored.endDate = Date()
  254. newTempTargetRunStored
  255. .target = canceledTempTarget.target ?? 0
  256. newTempTargetRunStored.tempTarget = canceledTempTarget
  257. newTempTargetRunStored.isUploadedToNS = false
  258. }
  259. }
  260. // Disable all override except the one with overrideID
  261. for tempTargetToCancel in results {
  262. if tempTargetToCancel.objectID != id {
  263. tempTargetToCancel.enabled = false
  264. }
  265. }
  266. // Save the context if there are changes
  267. if self.viewContext.hasChanges {
  268. try self.viewContext.save()
  269. // Update the storage
  270. self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
  271. }
  272. } catch {
  273. debugPrint(
  274. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
  275. )
  276. }
  277. }
  278. }
  279. /// Duplicates the current preset and cancels the previous one.
  280. @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
  281. // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
  282. guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
  283. tempTargetPresetToDuplicate.isPreset == true else { return }
  284. // Copy the current TempTarget-Preset to not edit the underlying Preset
  285. let duplidateId = await tempTargetStorage.copyRunningTempTarget(tempTargetPresetToDuplicate)
  286. // Cancel the duplicated Temp Target
  287. // As we are on the Main Thread already we don't need to cancel via the objectID in this case
  288. do {
  289. try await viewContext.perform {
  290. tempTargetPresetToDuplicate.enabled = false
  291. guard self.viewContext.hasChanges else { return }
  292. try self.viewContext.save()
  293. }
  294. if let tempTargetToEdit = try viewContext.existingObject(with: duplidateId) as? TempTargetStored
  295. {
  296. currentActiveTempTarget = tempTargetToEdit
  297. activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
  298. }
  299. } catch {
  300. debugPrint(
  301. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
  302. )
  303. }
  304. }
  305. /// Deletes a Temp Target preset.
  306. func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
  307. await tempTargetStorage.deleteOverridePreset(objectID)
  308. setupTempTargetPresetsArray()
  309. }
  310. /// Resets Temp Target state variables.
  311. @MainActor func resetTempTargetState() async {
  312. tempTargetName = ""
  313. tempTargetTarget = 100
  314. tempTargetDuration = 0
  315. percentage = 100
  316. halfBasalTarget = settingHalfBasalTarget
  317. }
  318. // MARK: - Calculations
  319. /// Computes the half-basal target based on the current settings.
  320. func computeHalfBasalTarget(
  321. usingTarget initialTarget: Decimal? = nil,
  322. usingPercentage initialPercentage: Double? = nil
  323. ) -> Double {
  324. let adjustmentPercentage = initialPercentage ?? percentage
  325. let adjustmentRatio = Decimal(adjustmentPercentage / 100)
  326. let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
  327. var halfBasalTargetValue = halfBasalTarget
  328. if adjustmentRatio != 1 {
  329. halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
  330. (adjustmentRatio - 1)
  331. }
  332. return round(Double(halfBasalTargetValue))
  333. }
  334. /// Determines if sensitivity adjustment is enabled based on target.
  335. func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
  336. let target = initialTarget ?? tempTargetTarget
  337. if target < normalTarget, lowTTlowersSens { return true }
  338. if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
  339. return false
  340. }
  341. /// Computes the low value for the slider based on the target.
  342. func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
  343. let calcTarget = initialTarget ?? tempTargetTarget
  344. guard calcTarget != 0 else { return 15 } // oref defined maximum sensitivity
  345. let minSens = calcTarget < normalTarget ? 105 : 15
  346. return Double(max(0, minSens))
  347. }
  348. /// Computes the high value for the slider based on the target.
  349. func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
  350. let calcTarget = initialTarget ?? tempTargetTarget
  351. guard calcTarget != 0 else { return Double(maxValue * 100) } // oref defined limit for increased insulin delivery
  352. let maxSens = calcTarget > normalTarget ? 95 : Double(maxValue * 100)
  353. return maxSens
  354. }
  355. /// Computes the adjusted percentage for the slider.
  356. func computeAdjustedPercentage(
  357. usingHBT initialHalfBasalTarget: Decimal? = nil,
  358. usingTarget initialTarget: Decimal? = nil
  359. ) -> Double {
  360. let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
  361. let calcTarget = initialTarget ?? tempTargetTarget
  362. let deviationFromNormal = halfBasalTargetValue - normalTarget
  363. let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
  364. let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
  365. adjustmentFactor
  366. return Double(min(adjustmentRatio, maxValue) * 100).rounded()
  367. }
  368. }
  369. enum TempTargetSensitivityAdjustmentType: String, CaseIterable {
  370. case standard = "Standard"
  371. case slider = "Custom"
  372. }