AdjustmentsStateModel+TempTargets.swift 18 KB

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