TempTargetsStorage.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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 fetchScheduledTempTargets() async -> [NSManagedObjectID]
  13. func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
  14. func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
  15. func deleteOverridePreset(_ objectID: NSManagedObjectID) async
  16. func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
  17. func syncDate() -> Date
  18. func recent() -> [TempTarget]
  19. func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  20. func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  21. func presets() -> [TempTarget]
  22. func current() -> TempTarget?
  23. }
  24. final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
  25. private let processQueue = DispatchQueue(label: "BaseTempTargetsStorage.processQueue")
  26. @Injected() private var storage: FileStorage!
  27. @Injected() private var broadcaster: Broadcaster!
  28. @Injected() private var settingsManager: SettingsManager!
  29. private let backgroundContext = CoreDataStack.shared.newTaskContext()
  30. private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  31. init(resolver: Resolver) {
  32. injectServices(resolver)
  33. }
  34. func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
  35. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  36. ofType: TempTargetStored.self,
  37. onContext: backgroundContext,
  38. predicate: NSPredicate.lastActiveTempTarget,
  39. key: "orderPosition",
  40. ascending: true,
  41. fetchLimit: fetchLimit
  42. )
  43. guard let fetchedResults = results as? [TempTargetStored] else { return [] }
  44. return await backgroundContext.perform {
  45. return fetchedResults.map(\.objectID)
  46. }
  47. }
  48. /// Returns the NSManagedObjectID of the Temp Target Presets
  49. func fetchForTempTargetPresets() async -> [NSManagedObjectID] {
  50. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  51. ofType: TempTargetStored.self,
  52. onContext: backgroundContext,
  53. predicate: NSPredicate.allTempTargetPresets,
  54. key: "orderPosition",
  55. ascending: true
  56. )
  57. guard let fetchedResults = results as? [TempTargetStored] else { return [] }
  58. return await backgroundContext.perform {
  59. return fetchedResults.map(\.objectID)
  60. }
  61. }
  62. func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
  63. let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
  64. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  65. ofType: TempTargetStored.self,
  66. onContext: backgroundContext,
  67. predicate: scheduledTempTargets,
  68. key: "date",
  69. ascending: false
  70. )
  71. guard let fetchedResults = results as? [TempTargetStored] else { return [] }
  72. return await backgroundContext.perform {
  73. return fetchedResults.map(\.objectID)
  74. }
  75. }
  76. func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
  77. let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
  78. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  79. ofType: TempTargetStored.self,
  80. onContext: backgroundContext,
  81. predicate: predicate,
  82. key: "date",
  83. ascending: false,
  84. fetchLimit: 1
  85. )
  86. guard let fetchedResults = results as? [TempTargetStored] else { return [] }
  87. return await backgroundContext.perform {
  88. fetchedResults.map(\.objectID)
  89. }
  90. }
  91. func storeTempTarget(tempTarget: TempTarget) async {
  92. var presetCount = -1
  93. if tempTarget.isPreset == true {
  94. let presets = await fetchForTempTargetPresets()
  95. presetCount = presets.count
  96. }
  97. await backgroundContext.perform {
  98. let newTempTarget = TempTargetStored(context: self.backgroundContext)
  99. newTempTarget.date = tempTarget.createdAt
  100. newTempTarget.id = UUID()
  101. newTempTarget.enabled = tempTarget.enabled ?? false
  102. newTempTarget.duration = tempTarget.duration as NSDecimalNumber
  103. newTempTarget.isUploadedToNS = false
  104. newTempTarget.name = tempTarget.name
  105. newTempTarget.target = NSDecimalNumber(decimal: tempTarget.targetTop ?? 0)
  106. newTempTarget.isPreset = tempTarget.isPreset ?? false
  107. // Nullify half basal target to ensure the latest HBT is used via OpenAPS Manager when sending TT data to oref
  108. newTempTarget.halfBasalTarget = nil
  109. if let halfBasalTarget = tempTarget.halfBasalTarget,
  110. halfBasalTarget != self.settingsManager.preferences.halfBasalExerciseTarget
  111. {
  112. newTempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalTarget)
  113. }
  114. if tempTarget.isPreset == true, presetCount > -1 {
  115. newTempTarget.orderPosition = Int16(presetCount + 1)
  116. }
  117. do {
  118. guard self.backgroundContext.hasChanges else { return }
  119. try self.backgroundContext.save()
  120. } catch let error as NSError {
  121. debugPrint(
  122. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target to Core Data with error: \(error.userInfo)"
  123. )
  124. }
  125. }
  126. }
  127. func saveTempTargetsToStorage(_ targets: [TempTarget]) {
  128. processQueue.async {
  129. let file = OpenAPS.Settings.tempTargets
  130. var uniqEvents: [TempTarget] = []
  131. self.storage.transaction { storage in
  132. storage.append(targets, to: file, uniqBy: \.createdAt)
  133. let retrievedTargets = storage.retrieve(file, as: [TempTarget].self) ?? []
  134. uniqEvents = retrievedTargets
  135. .filter { $0.isWithinLastDay }
  136. .sorted(by: { $0.createdAt > $1.createdAt })
  137. storage.save(uniqEvents, as: file)
  138. }
  139. self.broadcaster.notify(TempTargetsObserver.self, on: self.processQueue) {
  140. $0.tempTargetsDidUpdate(uniqEvents)
  141. }
  142. }
  143. }
  144. // Copy the current Temp Target if it is a RUNNING Preset
  145. /// otherwise we would edit the Preset
  146. @MainActor func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID {
  147. let newTempTarget = TempTargetStored(context: viewContext)
  148. newTempTarget.date = tempTarget.date
  149. newTempTarget.id = tempTarget.id
  150. newTempTarget.enabled = tempTarget.enabled
  151. newTempTarget.duration = tempTarget.duration
  152. newTempTarget.isUploadedToNS = true // to avoid getting duplicates on NS
  153. newTempTarget.name = tempTarget.name
  154. newTempTarget.target = tempTarget.target
  155. newTempTarget.isPreset = false // no Preset
  156. newTempTarget.halfBasalTarget = tempTarget.halfBasalTarget != 160 ? tempTarget.halfBasalTarget : nil
  157. await viewContext.perform {
  158. do {
  159. guard self.viewContext.hasChanges else { return }
  160. try self.viewContext.save()
  161. } catch let error as NSError {
  162. debugPrint(
  163. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to copy Temp Target with error: \(error.userInfo)"
  164. )
  165. }
  166. }
  167. return newTempTarget.objectID
  168. }
  169. @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
  170. await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
  171. }
  172. func syncDate() -> Date {
  173. Date().addingTimeInterval(-1.days.timeInterval)
  174. }
  175. func recent() -> [TempTarget] {
  176. storage.retrieve(OpenAPS.Settings.tempTargets, as: [TempTarget].self)?.reversed() ?? []
  177. }
  178. func current() -> TempTarget? {
  179. guard let last = recent().last else {
  180. return nil
  181. }
  182. guard last.createdAt.addingTimeInterval(Int(last.duration).minutes.timeInterval) > Date(), last.createdAt <= Date(),
  183. last.duration != 0
  184. else {
  185. return nil
  186. }
  187. return last
  188. }
  189. func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  190. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  191. ofType: TempTargetStored.self,
  192. onContext: backgroundContext,
  193. predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout, // TODO: create adjustment predicate (OR+TT)
  194. key: "date",
  195. ascending: false
  196. )
  197. return await backgroundContext.perform {
  198. guard let fetchedTempTargets = results as? [TempTargetStored] else { return [] }
  199. return fetchedTempTargets.map { tempTarget in
  200. NightscoutTreatment(
  201. duration: Int(truncating: tempTarget.duration ?? 60),
  202. rawDuration: nil,
  203. rawRate: nil,
  204. absolute: nil,
  205. rate: nil,
  206. eventType: .nsTempTarget,
  207. createdAt: tempTarget.date ?? Date(),
  208. enteredBy: TempTarget.manual,
  209. bolus: nil,
  210. insulin: nil,
  211. notes: tempTarget.name ?? "Custom Temporary Target",
  212. carbs: nil,
  213. targetTop: tempTarget
  214. .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL),
  215. targetBottom: tempTarget
  216. .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL)
  217. )
  218. }
  219. }
  220. }
  221. func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  222. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  223. ofType: TempTargetRunStored.self,
  224. onContext: backgroundContext,
  225. predicate: NSPredicate(
  226. format: "startDate >= %@ AND isUploadedToNS == %@",
  227. Date.oneDayAgo as NSDate,
  228. false as NSNumber
  229. ),
  230. key: "startDate",
  231. ascending: false
  232. )
  233. return await backgroundContext.perform {
  234. guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else { return [] }
  235. return fetchedTempTargetRuns.map { tempTargetRun in
  236. var durationInMinutes = (tempTargetRun.endDate?.timeIntervalSince(tempTargetRun.startDate ?? Date()) ?? 1) / 60
  237. durationInMinutes = durationInMinutes < 1 ? 1 : durationInMinutes
  238. return NightscoutTreatment(
  239. duration: Int(durationInMinutes),
  240. rawDuration: nil,
  241. rawRate: nil,
  242. absolute: nil,
  243. rate: nil,
  244. eventType: .nsTempTarget,
  245. createdAt: (tempTargetRun.startDate ?? tempTargetRun.tempTarget?.date) ?? Date(),
  246. enteredBy: TempTarget.manual,
  247. bolus: nil,
  248. insulin: nil,
  249. notes: nil,
  250. carbs: nil,
  251. targetTop: tempTargetRun
  252. .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL),
  253. targetBottom: tempTargetRun
  254. .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL)
  255. )
  256. }
  257. }
  258. }
  259. func presets() -> [TempTarget] {
  260. storage.retrieve(OpenAPS.FreeAPS.tempTargetsPresets, as: [TempTarget].self)?.reversed() ?? []
  261. }
  262. }
  263. private extension TempTarget {
  264. var isActive: Bool {
  265. let expirationTime = createdAt.addingTimeInterval(Int(duration).minutes.timeInterval)
  266. return expirationTime > Date() && createdAt <= Date()
  267. }
  268. var isWithinLastDay: Bool {
  269. createdAt.addingTimeInterval(1.days.timeInterval) > Date()
  270. }
  271. }