TempTargetsStorage.swift 13 KB

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