TempTargetsStorage.swift 8.1 KB

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