DosingDecisionStore.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. //
  2. // DosingDecisionStore.swift
  3. // LoopKit
  4. //
  5. // Created by Darin Krauss on 10/14/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import os.log
  9. import Foundation
  10. import CoreData
  11. public protocol DosingDecisionStoreDelegate: AnyObject {
  12. /**
  13. Informs the delegate that the dosing decision store has updated dosing decision data.
  14. - Parameter dosingDecisionStore: The dosing decision store that has updated dosing decision data.
  15. */
  16. func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore)
  17. }
  18. public class DosingDecisionStore {
  19. public weak var delegate: DosingDecisionStoreDelegate?
  20. private let store: PersistenceController
  21. private let expireAfter: TimeInterval
  22. private let dataAccessQueue = DispatchQueue(label: "com.loopkit.DosingDecisionStore.dataAccessQueue", qos: .utility)
  23. public let log = OSLog(category: "DosingDecisionStore")
  24. public init(store: PersistenceController, expireAfter: TimeInterval) {
  25. self.store = store
  26. self.expireAfter = expireAfter
  27. }
  28. public func storeDosingDecisionData(_ dosingDecisionData: StoredDosingDecisionData, completion: @escaping () -> Void) {
  29. dataAccessQueue.async {
  30. self.store.managedObjectContext.performAndWait {
  31. let object = DosingDecisionObject(context: self.store.managedObjectContext)
  32. object.date = dosingDecisionData.date
  33. object.data = dosingDecisionData.data
  34. self.store.save()
  35. }
  36. self.purgeExpiredDosingDecisions()
  37. completion()
  38. }
  39. }
  40. public var expireDate: Date {
  41. return Date(timeIntervalSinceNow: -expireAfter)
  42. }
  43. private func purgeExpiredDosingDecisions() {
  44. purgeDosingDecisionObjects(before: expireDate)
  45. }
  46. public func purgeDosingDecisions(before date: Date, completion: @escaping (Error?) -> Void) {
  47. dataAccessQueue.async {
  48. self.purgeDosingDecisionObjects(before: date, completion: completion)
  49. }
  50. }
  51. private func purgeDosingDecisionObjects(before date: Date, completion: ((Error?) -> Void)? = nil) {
  52. dispatchPrecondition(condition: .onQueue(dataAccessQueue))
  53. var purgeError: Error?
  54. store.managedObjectContext.performAndWait {
  55. do {
  56. let count = try self.store.managedObjectContext.purgeObjects(of: DosingDecisionObject.self, matching: NSPredicate(format: "date < %@", date as NSDate))
  57. self.log.info("Purged %d DosingDecisionObjects", count)
  58. } catch let error {
  59. self.log.error("Unable to purge DosingDecisionObjects: %{public}@", String(describing: error))
  60. purgeError = error
  61. }
  62. }
  63. if let purgeError = purgeError {
  64. completion?(purgeError)
  65. return
  66. }
  67. delegate?.dosingDecisionStoreHasUpdatedDosingDecisionData(self)
  68. completion?(nil)
  69. }
  70. }
  71. extension DosingDecisionStore {
  72. public struct QueryAnchor: RawRepresentable {
  73. public typealias RawValue = [String: Any]
  74. internal var modificationCounter: Int64
  75. public init() {
  76. self.modificationCounter = 0
  77. }
  78. public init?(rawValue: RawValue) {
  79. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  80. return nil
  81. }
  82. self.modificationCounter = modificationCounter
  83. }
  84. public var rawValue: RawValue {
  85. var rawValue: RawValue = [:]
  86. rawValue["modificationCounter"] = modificationCounter
  87. return rawValue
  88. }
  89. }
  90. public enum DosingDecisionDataQueryResult {
  91. case success(QueryAnchor, [StoredDosingDecisionData])
  92. case failure(Error)
  93. }
  94. public func executeDosingDecisionDataQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (DosingDecisionDataQueryResult) -> Void) {
  95. dataAccessQueue.async {
  96. var queryAnchor = queryAnchor ?? QueryAnchor()
  97. var queryResult = [StoredDosingDecisionData]()
  98. var queryError: Error?
  99. guard limit > 0 else {
  100. completion(.success(queryAnchor, queryResult))
  101. return
  102. }
  103. self.store.managedObjectContext.performAndWait {
  104. let storedRequest: NSFetchRequest<DosingDecisionObject> = DosingDecisionObject.fetchRequest()
  105. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  106. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  107. storedRequest.fetchLimit = limit
  108. do {
  109. let stored = try self.store.managedObjectContext.fetch(storedRequest)
  110. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  111. queryAnchor.modificationCounter = modificationCounter
  112. }
  113. queryResult.append(contentsOf: stored.compactMap { StoredDosingDecisionData(date: $0.date, data: $0.data) })
  114. } catch let error {
  115. queryError = error
  116. return
  117. }
  118. }
  119. if let queryError = queryError {
  120. completion(.failure(queryError))
  121. return
  122. }
  123. completion(.success(queryAnchor, queryResult))
  124. }
  125. }
  126. }
  127. public struct StoredDosingDecisionData {
  128. public let date: Date
  129. public let data: Data
  130. public init(date: Date, data: Data) {
  131. self.date = date
  132. self.data = data
  133. }
  134. }
  135. public struct StoredDosingDecision {
  136. public let date: Date
  137. public let insulinOnBoard: InsulinValue?
  138. public let carbsOnBoard: CarbValue?
  139. public let scheduleOverride: TemporaryScheduleOverride?
  140. public let glucoseTargetRangeSchedule: GlucoseRangeSchedule?
  141. public let effectiveGlucoseTargetRangeSchedule: GlucoseRangeSchedule?
  142. public let predictedGlucose: [PredictedGlucoseValue]?
  143. public let predictedGlucoseIncludingPendingInsulin: [PredictedGlucoseValue]?
  144. public let lastReservoirValue: LastReservoirValue?
  145. public let manualGlucose: SimpleGlucoseValue?
  146. public let originalCarbEntry: StoredCarbEntry?
  147. public let carbEntry: StoredCarbEntry?
  148. public let automaticDoseRecommendation: AutomaticDoseRecommendationWithDate?
  149. public let recommendedBolus: BolusRecommendationWithDate?
  150. public let requestedBolus: Double?
  151. public let pumpManagerStatus: PumpManagerStatus?
  152. public let notificationSettings: NotificationSettings?
  153. public let deviceSettings: DeviceSettings?
  154. public let errors: [Error]?
  155. public let syncIdentifier: String
  156. public init(date: Date = Date(),
  157. insulinOnBoard: InsulinValue? = nil,
  158. carbsOnBoard: CarbValue? = nil,
  159. scheduleOverride: TemporaryScheduleOverride? = nil,
  160. glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
  161. effectiveGlucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
  162. predictedGlucose: [PredictedGlucoseValue]? = nil,
  163. predictedGlucoseIncludingPendingInsulin: [PredictedGlucoseValue]? = nil,
  164. lastReservoirValue: LastReservoirValue? = nil,
  165. manualGlucose: SimpleGlucoseValue? = nil,
  166. originalCarbEntry: StoredCarbEntry? = nil,
  167. carbEntry: StoredCarbEntry? = nil,
  168. automaticDoseRecommendation: AutomaticDoseRecommendationWithDate? = nil,
  169. recommendedBolus: BolusRecommendationWithDate? = nil,
  170. requestedBolus: Double? = nil,
  171. pumpManagerStatus: PumpManagerStatus? = nil,
  172. notificationSettings: NotificationSettings? = nil,
  173. deviceSettings: DeviceSettings? = nil,
  174. errors: [Error]? = nil,
  175. syncIdentifier: String = UUID().uuidString) {
  176. self.date = date
  177. self.insulinOnBoard = insulinOnBoard
  178. self.carbsOnBoard = carbsOnBoard
  179. self.scheduleOverride = scheduleOverride
  180. self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
  181. self.effectiveGlucoseTargetRangeSchedule = effectiveGlucoseTargetRangeSchedule
  182. self.predictedGlucose = predictedGlucose
  183. self.predictedGlucoseIncludingPendingInsulin = predictedGlucoseIncludingPendingInsulin
  184. self.lastReservoirValue = lastReservoirValue
  185. self.manualGlucose = manualGlucose
  186. self.originalCarbEntry = originalCarbEntry
  187. self.carbEntry = carbEntry
  188. self.automaticDoseRecommendation = automaticDoseRecommendation
  189. self.recommendedBolus = recommendedBolus
  190. self.requestedBolus = requestedBolus
  191. self.pumpManagerStatus = pumpManagerStatus
  192. self.notificationSettings = notificationSettings
  193. self.deviceSettings = deviceSettings
  194. self.errors = errors
  195. self.syncIdentifier = syncIdentifier
  196. }
  197. public struct LastReservoirValue: Codable {
  198. public let startDate: Date
  199. public let unitVolume: Double
  200. public init(startDate: Date, unitVolume: Double) {
  201. self.startDate = startDate
  202. self.unitVolume = unitVolume
  203. }
  204. }
  205. public struct AutomaticDoseRecommendationWithDate: Codable {
  206. public let recommendation: AutomaticDoseRecommendation
  207. public let date: Date
  208. public init(recommendation: AutomaticDoseRecommendation, date: Date) {
  209. self.recommendation = recommendation
  210. self.date = date
  211. }
  212. }
  213. public struct BolusRecommendationWithDate: Codable {
  214. public let recommendation: ManualBolusRecommendation
  215. public let date: Date
  216. public init(recommendation: ManualBolusRecommendation, date: Date) {
  217. self.recommendation = recommendation
  218. self.date = date
  219. }
  220. }
  221. public struct DeviceSettings: Codable, Equatable {
  222. let name: String
  223. let systemName: String
  224. let systemVersion: String
  225. let model: String
  226. let modelIdentifier: String
  227. let batteryLevel: Float?
  228. let batteryState: BatteryState?
  229. public init(name: String, systemName: String, systemVersion: String, model: String, modelIdentifier: String, batteryLevel: Float? = nil, batteryState: BatteryState? = nil) {
  230. self.name = name
  231. self.systemName = systemName
  232. self.systemVersion = systemVersion
  233. self.model = model
  234. self.modelIdentifier = modelIdentifier
  235. self.batteryLevel = batteryLevel
  236. self.batteryState = batteryState
  237. }
  238. public enum BatteryState: String, Codable {
  239. case unknown
  240. case unplugged
  241. case charging
  242. case full
  243. }
  244. }
  245. }
  246. // MARK: - Critical Event Log Export
  247. extension DosingDecisionStore {
  248. private var exportProgressUnitCountPerObject: Int64 { 33 }
  249. private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
  250. public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
  251. var result: Result<Int64, Error>?
  252. self.store.managedObjectContext.performAndWait {
  253. do {
  254. let request: NSFetchRequest<DosingDecisionObject> = DosingDecisionObject.fetchRequest()
  255. request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
  256. let objectCount = try self.store.managedObjectContext.count(for: request)
  257. result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
  258. } catch let error {
  259. result = .failure(error)
  260. }
  261. }
  262. return result!
  263. }
  264. public func export(startDate: Date, endDate: Date, using encoder: @escaping ([DosingDecisionObject]) throws -> Void, progress: Progress) -> Error? {
  265. var modificationCounter: Int64 = 0
  266. var fetching = true
  267. var error: Error?
  268. while fetching && error == nil {
  269. self.store.managedObjectContext.performAndWait {
  270. do {
  271. guard !progress.isCancelled else {
  272. throw CriticalEventLogError.cancelled
  273. }
  274. let request: NSFetchRequest<DosingDecisionObject> = DosingDecisionObject.fetchRequest()
  275. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "modificationCounter > %d", modificationCounter),
  276. self.exportDatePredicate(startDate: startDate, endDate: endDate)])
  277. request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  278. request.fetchLimit = self.exportFetchLimit
  279. let objects = try self.store.managedObjectContext.fetch(request)
  280. if objects.isEmpty {
  281. fetching = false
  282. return
  283. }
  284. try encoder(objects)
  285. modificationCounter = objects.last!.modificationCounter
  286. progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
  287. } catch let fetchError {
  288. error = fetchError
  289. }
  290. }
  291. }
  292. return error
  293. }
  294. private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
  295. var predicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  296. if let endDate = endDate {
  297. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "date < %@", endDate as NSDate)])
  298. }
  299. return predicate
  300. }
  301. }
  302. // MARK: - Core Data (Bulk) - TEST ONLY
  303. extension DosingDecisionStore {
  304. public func addStoredDosingDecisionDatas(dosingDecisionDatas: [StoredDosingDecisionData], completion: @escaping (Error?) -> Void) {
  305. guard !dosingDecisionDatas.isEmpty else {
  306. completion(nil)
  307. return
  308. }
  309. dataAccessQueue.async {
  310. var error: Error?
  311. self.store.managedObjectContext.performAndWait {
  312. for dosingDecisionData in dosingDecisionDatas {
  313. let object = DosingDecisionObject(context: self.store.managedObjectContext)
  314. object.date = dosingDecisionData.date
  315. object.data = dosingDecisionData.data
  316. }
  317. error = self.store.save()
  318. }
  319. guard error == nil else {
  320. completion(error)
  321. return
  322. }
  323. self.log.info("Added %d DosingDecisionObjects", dosingDecisionDatas.count)
  324. self.delegate?.dosingDecisionStoreHasUpdatedDosingDecisionData(self)
  325. completion(nil)
  326. }
  327. }
  328. }