TempTargetsStorage.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import CoreData
  2. import Foundation
  3. import SwiftDate
  4. import Swinject
  5. protocol TempTargetsObserver {
  6. func tempTargetsDidUpdate(_ targets: [TempTarget])
  7. }
  8. protocol TempTargetsStorage {
  9. func storeTempTarget(tempTarget: TempTarget) async
  10. func storePresets(_ targets: [TempTarget])
  11. func saveTempTargetsToStorage(_ targets: [TempTarget], isPreset: Bool)
  12. func fetchForTempTargetPresets() async -> [NSManagedObjectID]
  13. func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
  14. func deleteOverridePreset(_ objectID: NSManagedObjectID) async
  15. func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
  16. func syncDate() -> Date
  17. func recent() -> [TempTarget]
  18. func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment]
  19. func presets() -> [TempTarget]
  20. func current() -> TempTarget?
  21. }
  22. final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
  23. private let processQueue = DispatchQueue(label: "BaseTempTargetsStorage.processQueue")
  24. @Injected() private var storage: FileStorage!
  25. @Injected() private var broadcaster: Broadcaster!
  26. private let backgroundContext = CoreDataStack.shared.newTaskContext()
  27. private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  28. init(resolver: Resolver) {
  29. injectServices(resolver)
  30. }
  31. func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
  32. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  33. ofType: TempTargetStored.self,
  34. onContext: backgroundContext,
  35. predicate: NSPredicate.lastActiveTempTarget,
  36. key: "date",
  37. ascending: true,
  38. fetchLimit: fetchLimit
  39. )
  40. guard let fetchedResults = results as? [TempTargetStored] else { return [] }
  41. return await backgroundContext.perform {
  42. return fetchedResults.map(\.objectID)
  43. }
  44. }
  45. /// Returns the NSManagedObjectID of the Temp Target Presets
  46. func fetchForTempTargetPresets() async -> [NSManagedObjectID] {
  47. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  48. ofType: TempTargetStored.self,
  49. onContext: backgroundContext,
  50. predicate: NSPredicate.allTempTargetPresets,
  51. key: "date",
  52. ascending: true
  53. )
  54. guard let fetchedResults = results as? [TempTargetStored] else { return [] }
  55. return await backgroundContext.perform {
  56. return fetchedResults.map(\.objectID)
  57. }
  58. }
  59. func storeTempTarget(tempTarget: TempTarget) async {
  60. await backgroundContext.perform {
  61. let newTempTarget = TempTargetStored(context: self.backgroundContext)
  62. newTempTarget.date = tempTarget.createdAt
  63. newTempTarget.id = UUID()
  64. newTempTarget.enabled = tempTarget.enabled ?? false
  65. newTempTarget.duration = tempTarget.duration as NSDecimalNumber
  66. newTempTarget.isUploadedToNS = false
  67. newTempTarget.name = tempTarget.name
  68. newTempTarget.target = NSDecimalNumber(decimal: tempTarget.targetTop ?? 0)
  69. newTempTarget.isPreset = tempTarget.isPreset ?? false
  70. do {
  71. guard self.backgroundContext.hasChanges else { return }
  72. try self.backgroundContext.save()
  73. } catch let error as NSError {
  74. debugPrint(
  75. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target to Core Data with error: \(error.userInfo)"
  76. )
  77. }
  78. /*
  79. Saving the Preset to the Storage means that it gets used by Oref
  80. We only want that when either creating a new non-Preset-Temp Target or when enacting a Temp Target Preset, NOT when we are only saving a new Preset, hence the check here!
  81. */
  82. if !(tempTarget.isPreset ?? false) {
  83. self.saveTempTargetsToStorage([tempTarget], isPreset: false)
  84. }
  85. }
  86. }
  87. func saveTempTargetsToStorage(_ targets: [TempTarget], isPreset: Bool) {
  88. processQueue.sync {
  89. var updatedTargets = targets
  90. if !isPreset, let currentTarget = current() {
  91. if let newActive = updatedTargets.last(where: { $0.isActive }) {
  92. // Cancel current target
  93. updatedTargets.append(.cancel(at: newActive.createdAt.addingTimeInterval(-1)))
  94. }
  95. }
  96. let file = isPreset ? OpenAPS.FreeAPS.tempTargetsPresets : OpenAPS.Settings.tempTargets
  97. var uniqEvents: [TempTarget] = []
  98. self.storage.transaction { storage in
  99. storage.append(updatedTargets, to: file, uniqBy: \.createdAt)
  100. let retrievedTargets = storage.retrieve(file, as: [TempTarget].self) ?? []
  101. uniqEvents = retrievedTargets
  102. .filter { isPreset || $0.isWithinLastDay }
  103. .sorted(by: { $0.createdAt > $1.createdAt })
  104. storage.save(uniqEvents, as: file)
  105. }
  106. broadcaster.notify(TempTargetsObserver.self, on: processQueue) {
  107. $0.tempTargetsDidUpdate(uniqEvents)
  108. }
  109. }
  110. }
  111. func storePresets(_ targets: [TempTarget]) {
  112. storage.remove(OpenAPS.FreeAPS.tempTargetsPresets)
  113. saveTempTargetsToStorage(targets, isPreset: false)
  114. }
  115. // Copy the current Temp Target if it is a RUNNING Preset
  116. /// otherwise we would edit the Preset
  117. @MainActor func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID {
  118. let newTempTarget = TempTargetStored(context: viewContext)
  119. newTempTarget.date = tempTarget.date
  120. newTempTarget.id = tempTarget.id
  121. newTempTarget.enabled = tempTarget.enabled
  122. newTempTarget.duration = tempTarget.duration
  123. newTempTarget.isUploadedToNS = true // to avoid getting duplicates on NS
  124. newTempTarget.name = tempTarget.name
  125. newTempTarget.target = tempTarget.target
  126. newTempTarget.isPreset = false // no Preset
  127. await viewContext.perform {
  128. do {
  129. guard self.viewContext.hasChanges else { return }
  130. try self.viewContext.save()
  131. } catch let error as NSError {
  132. debugPrint(
  133. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to copy Temp Target with error: \(error.userInfo)"
  134. )
  135. }
  136. }
  137. return newTempTarget.objectID
  138. }
  139. @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
  140. await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
  141. }
  142. func syncDate() -> Date {
  143. Date().addingTimeInterval(-1.days.timeInterval)
  144. }
  145. func recent() -> [TempTarget] {
  146. storage.retrieve(OpenAPS.Settings.tempTargets, as: [TempTarget].self)?.reversed() ?? []
  147. }
  148. func current() -> TempTarget? {
  149. guard let last = recent().last else {
  150. return nil
  151. }
  152. guard last.createdAt.addingTimeInterval(Int(last.duration).minutes.timeInterval) > Date(), last.createdAt <= Date(),
  153. last.duration != 0
  154. else {
  155. return nil
  156. }
  157. return last
  158. }
  159. func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment] {
  160. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedTempTargets, as: [NightscoutTreatment].self) ?? []
  161. let eventsManual = recent().filter { $0.enteredBy == TempTarget.manual }
  162. let treatments = eventsManual.map {
  163. NightscoutTreatment(
  164. duration: Int($0.duration),
  165. rawDuration: nil,
  166. rawRate: nil,
  167. absolute: nil,
  168. rate: nil,
  169. eventType: .nsTempTarget,
  170. createdAt: $0.createdAt,
  171. enteredBy: TempTarget.manual,
  172. bolus: nil,
  173. insulin: nil,
  174. notes: nil,
  175. carbs: nil,
  176. targetTop: $0.targetTop,
  177. targetBottom: $0.targetBottom
  178. )
  179. }
  180. return Array(Set(treatments).subtracting(Set(uploaded)))
  181. }
  182. func presets() -> [TempTarget] {
  183. storage.retrieve(OpenAPS.FreeAPS.tempTargetsPresets, as: [TempTarget].self)?.reversed() ?? []
  184. }
  185. }
  186. private extension TempTarget {
  187. var isActive: Bool {
  188. let expirationTime = createdAt.addingTimeInterval(Int(duration).minutes.timeInterval)
  189. return expirationTime > Date() && createdAt <= Date()
  190. }
  191. var isWithinLastDay: Bool {
  192. createdAt.addingTimeInterval(1.days.timeInterval) > Date()
  193. }
  194. }