AdjustmentsStateModel+TempTargets.swift 18 KB

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