DosingDecisionStore.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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 storeDosingDecision(_ dosingDecision: StoredDosingDecision, completion: @escaping () -> Void) {
  29. dataAccessQueue.async {
  30. if let data = self.encodeDosingDecision(dosingDecision) {
  31. self.store.managedObjectContext.performAndWait {
  32. let object = DosingDecisionObject(context: self.store.managedObjectContext)
  33. object.data = data
  34. object.date = dosingDecision.date
  35. self.store.save()
  36. }
  37. }
  38. self.purgeExpiredDosingDecisions()
  39. completion()
  40. }
  41. }
  42. public var expireDate: Date {
  43. return Date(timeIntervalSinceNow: -expireAfter)
  44. }
  45. private func purgeExpiredDosingDecisions() {
  46. purgeDosingDecisionObjects(before: expireDate)
  47. }
  48. public func purgeDosingDecisions(before date: Date, completion: @escaping (Error?) -> Void) {
  49. dataAccessQueue.async {
  50. self.purgeDosingDecisionObjects(before: date, completion: completion)
  51. }
  52. }
  53. private func purgeDosingDecisionObjects(before date: Date, completion: ((Error?) -> Void)? = nil) {
  54. dispatchPrecondition(condition: .onQueue(dataAccessQueue))
  55. var purgeError: Error?
  56. store.managedObjectContext.performAndWait {
  57. do {
  58. let count = try self.store.managedObjectContext.purgeObjects(of: DosingDecisionObject.self, matching: NSPredicate(format: "date < %@", date as NSDate))
  59. self.log.info("Purged %d DosingDecisionObjects", count)
  60. } catch let error {
  61. self.log.error("Unable to purge DosingDecisionObjects: %{public}@", String(describing: error))
  62. purgeError = error
  63. }
  64. }
  65. if let purgeError = purgeError {
  66. completion?(purgeError)
  67. return
  68. }
  69. delegate?.dosingDecisionStoreHasUpdatedDosingDecisionData(self)
  70. completion?(nil)
  71. }
  72. private static var encoder: PropertyListEncoder = {
  73. let encoder = PropertyListEncoder()
  74. encoder.outputFormat = .binary
  75. return encoder
  76. }()
  77. private func encodeDosingDecision(_ dosingDecision: StoredDosingDecision) -> Data? {
  78. do {
  79. return try Self.encoder.encode(dosingDecision)
  80. } catch let error {
  81. self.log.error("Error encoding StoredDosingDecision: %@", String(describing: error))
  82. return nil
  83. }
  84. }
  85. private static var decoder = PropertyListDecoder()
  86. private func decodeDosingDecision(fromData data: Data) -> StoredDosingDecision? {
  87. do {
  88. return try Self.decoder.decode(StoredDosingDecision.self, from: data)
  89. } catch let error {
  90. self.log.error("Error decoding StoredDosingDecision: %@", String(describing: error))
  91. return nil
  92. }
  93. }
  94. }
  95. extension DosingDecisionStore {
  96. public struct QueryAnchor: Equatable, RawRepresentable {
  97. public typealias RawValue = [String: Any]
  98. internal var modificationCounter: Int64
  99. public init() {
  100. self.modificationCounter = 0
  101. }
  102. public init?(rawValue: RawValue) {
  103. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  104. return nil
  105. }
  106. self.modificationCounter = modificationCounter
  107. }
  108. public var rawValue: RawValue {
  109. var rawValue: RawValue = [:]
  110. rawValue["modificationCounter"] = modificationCounter
  111. return rawValue
  112. }
  113. }
  114. public enum DosingDecisionQueryResult {
  115. case success(QueryAnchor, [StoredDosingDecision])
  116. case failure(Error)
  117. }
  118. public func executeDosingDecisionQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (DosingDecisionQueryResult) -> Void) {
  119. dataAccessQueue.async {
  120. var queryAnchor = queryAnchor ?? QueryAnchor()
  121. var queryResult = [StoredDosingDecisionData]()
  122. var queryError: Error?
  123. guard limit > 0 else {
  124. completion(.success(queryAnchor, []))
  125. return
  126. }
  127. self.store.managedObjectContext.performAndWait {
  128. let storedRequest: NSFetchRequest<DosingDecisionObject> = DosingDecisionObject.fetchRequest()
  129. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  130. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  131. storedRequest.fetchLimit = limit
  132. do {
  133. let stored = try self.store.managedObjectContext.fetch(storedRequest)
  134. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  135. queryAnchor.modificationCounter = modificationCounter
  136. }
  137. queryResult.append(contentsOf: stored.compactMap { StoredDosingDecisionData(date: $0.date, data: $0.data) })
  138. } catch let error {
  139. queryError = error
  140. return
  141. }
  142. }
  143. if let queryError = queryError {
  144. completion(.failure(queryError))
  145. return
  146. }
  147. // Decoding a large number of dosing decision can be very CPU intensive and may take considerable wall clock time.
  148. // Do not block DosingDecisionStore dataAccessQueue. Perform work and callback in global utility queue.
  149. DispatchQueue.global(qos: .utility).async {
  150. completion(.success(queryAnchor, queryResult.compactMap { self.decodeDosingDecision(fromData: $0.data) }))
  151. }
  152. }
  153. }
  154. }
  155. public struct StoredDosingDecisionData {
  156. public let date: Date
  157. public let data: Data
  158. public init(date: Date, data: Data) {
  159. self.date = date
  160. self.data = data
  161. }
  162. }
  163. public typealias HistoricalGlucoseValue = PredictedGlucoseValue
  164. public struct StoredDosingDecision {
  165. public var date: Date
  166. public var controllerTimeZone: TimeZone
  167. public var reason: String
  168. public var settings: Settings?
  169. public var scheduleOverride: TemporaryScheduleOverride?
  170. public var controllerStatus: ControllerStatus?
  171. public var pumpManagerStatus: PumpManagerStatus?
  172. public var pumpStatusHighlight: StoredDeviceHighlight?
  173. public var cgmManagerStatus: CGMManagerStatus?
  174. public var lastReservoirValue: LastReservoirValue?
  175. public var historicalGlucose: [HistoricalGlucoseValue]?
  176. public var originalCarbEntry: StoredCarbEntry?
  177. public var carbEntry: StoredCarbEntry?
  178. public var manualGlucoseSample: StoredGlucoseSample?
  179. public var carbsOnBoard: CarbValue?
  180. public var insulinOnBoard: InsulinValue?
  181. public var glucoseTargetRangeSchedule: GlucoseRangeSchedule?
  182. public var predictedGlucose: [PredictedGlucoseValue]?
  183. public var automaticDoseRecommendation: AutomaticDoseRecommendation?
  184. public var manualBolusRecommendation: ManualBolusRecommendationWithDate?
  185. public var manualBolusRequested: Double?
  186. public var warnings: [Issue]
  187. public var errors: [Issue]
  188. public var syncIdentifier: UUID
  189. public init(date: Date = Date(),
  190. controllerTimeZone: TimeZone = TimeZone.current,
  191. reason: String,
  192. settings: Settings? = nil,
  193. scheduleOverride: TemporaryScheduleOverride? = nil,
  194. controllerStatus: ControllerStatus? = nil,
  195. pumpManagerStatus: PumpManagerStatus? = nil,
  196. pumpStatusHighlight: StoredDeviceHighlight? = nil,
  197. cgmManagerStatus: CGMManagerStatus? = nil,
  198. lastReservoirValue: LastReservoirValue? = nil,
  199. historicalGlucose: [HistoricalGlucoseValue]? = nil,
  200. originalCarbEntry: StoredCarbEntry? = nil,
  201. carbEntry: StoredCarbEntry? = nil,
  202. manualGlucoseSample: StoredGlucoseSample? = nil,
  203. carbsOnBoard: CarbValue? = nil,
  204. insulinOnBoard: InsulinValue? = nil,
  205. glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
  206. predictedGlucose: [PredictedGlucoseValue]? = nil,
  207. automaticDoseRecommendation: AutomaticDoseRecommendation? = nil,
  208. manualBolusRecommendation: ManualBolusRecommendationWithDate? = nil,
  209. manualBolusRequested: Double? = nil,
  210. warnings: [Issue] = [],
  211. errors: [Issue] = [],
  212. syncIdentifier: UUID = UUID()) {
  213. self.date = date
  214. self.controllerTimeZone = controllerTimeZone
  215. self.reason = reason
  216. self.settings = settings
  217. self.scheduleOverride = scheduleOverride
  218. self.controllerStatus = controllerStatus
  219. self.pumpManagerStatus = pumpManagerStatus
  220. self.pumpStatusHighlight = pumpStatusHighlight
  221. self.cgmManagerStatus = cgmManagerStatus
  222. self.lastReservoirValue = lastReservoirValue
  223. self.historicalGlucose = historicalGlucose
  224. self.originalCarbEntry = originalCarbEntry
  225. self.carbEntry = carbEntry
  226. self.manualGlucoseSample = manualGlucoseSample
  227. self.carbsOnBoard = carbsOnBoard
  228. self.insulinOnBoard = insulinOnBoard
  229. self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
  230. self.predictedGlucose = predictedGlucose
  231. self.automaticDoseRecommendation = automaticDoseRecommendation
  232. self.manualBolusRecommendation = manualBolusRecommendation
  233. self.manualBolusRequested = manualBolusRequested
  234. self.warnings = warnings
  235. self.errors = errors
  236. self.syncIdentifier = syncIdentifier
  237. }
  238. public struct Settings: Codable, Equatable {
  239. public let syncIdentifier: UUID
  240. public init(syncIdentifier: UUID) {
  241. self.syncIdentifier = syncIdentifier
  242. }
  243. }
  244. public struct ControllerStatus: Codable, Equatable {
  245. public enum BatteryState: String, Codable {
  246. case unknown
  247. case unplugged
  248. case charging
  249. case full
  250. }
  251. public let batteryState: BatteryState?
  252. public let batteryLevel: Float?
  253. public init(batteryState: BatteryState? = nil, batteryLevel: Float? = nil) {
  254. self.batteryState = batteryState
  255. self.batteryLevel = batteryLevel
  256. }
  257. }
  258. public struct LastReservoirValue: Codable {
  259. public let startDate: Date
  260. public let unitVolume: Double
  261. public init(startDate: Date, unitVolume: Double) {
  262. self.startDate = startDate
  263. self.unitVolume = unitVolume
  264. }
  265. }
  266. public struct Issue: Codable, Equatable {
  267. public let id: String
  268. public let details: [String: String]?
  269. public init(id: String, details: [String: String]? = nil) {
  270. self.id = id
  271. self.details = details?.isEmpty == false ? details : nil
  272. }
  273. }
  274. public struct StoredDeviceHighlight: Codable, Equatable, DeviceStatusHighlight {
  275. public var localizedMessage: String
  276. public var imageName: String
  277. public var state: DeviceStatusHighlightState
  278. public init(localizedMessage: String, imageName: String, state: DeviceStatusHighlightState) {
  279. self.localizedMessage = localizedMessage
  280. self.imageName = imageName
  281. self.state = state
  282. }
  283. }
  284. }
  285. public struct ManualBolusRecommendationWithDate: Codable {
  286. public let recommendation: ManualBolusRecommendation
  287. public let date: Date
  288. public init(recommendation: ManualBolusRecommendation, date: Date) {
  289. self.recommendation = recommendation
  290. self.date = date
  291. }
  292. }
  293. extension StoredDosingDecision: Codable {
  294. public init(from decoder: Decoder) throws {
  295. let container = try decoder.container(keyedBy: CodingKeys.self)
  296. self.init(date: try container.decode(Date.self, forKey: .date),
  297. controllerTimeZone: try container.decode(TimeZone.self, forKey: .controllerTimeZone),
  298. reason: try container.decode(String.self, forKey: .reason),
  299. settings: try container.decodeIfPresent(Settings.self, forKey: .settings),
  300. scheduleOverride: try container.decodeIfPresent(TemporaryScheduleOverride.self, forKey: .scheduleOverride),
  301. controllerStatus: try container.decodeIfPresent(ControllerStatus.self, forKey: .controllerStatus),
  302. pumpManagerStatus: try container.decodeIfPresent(PumpManagerStatus.self, forKey: .pumpManagerStatus),
  303. pumpStatusHighlight: try container.decodeIfPresent(StoredDeviceHighlight.self, forKey: .pumpStatusHighlight),
  304. cgmManagerStatus: try container.decodeIfPresent(CGMManagerStatus.self, forKey: .cgmManagerStatus),
  305. lastReservoirValue: try container.decodeIfPresent(LastReservoirValue.self, forKey: .lastReservoirValue),
  306. historicalGlucose: try container.decodeIfPresent([HistoricalGlucoseValue].self, forKey: .historicalGlucose),
  307. originalCarbEntry: try container.decodeIfPresent(StoredCarbEntry.self, forKey: .originalCarbEntry),
  308. carbEntry: try container.decodeIfPresent(StoredCarbEntry.self, forKey: .carbEntry),
  309. manualGlucoseSample: try container.decodeIfPresent(StoredGlucoseSample.self, forKey: .manualGlucoseSample),
  310. carbsOnBoard: try container.decodeIfPresent(CarbValue.self, forKey: .carbsOnBoard),
  311. insulinOnBoard: try container.decodeIfPresent(InsulinValue.self, forKey: .insulinOnBoard),
  312. glucoseTargetRangeSchedule: try container.decodeIfPresent(GlucoseRangeSchedule.self, forKey: .glucoseTargetRangeSchedule),
  313. predictedGlucose: try container.decodeIfPresent([PredictedGlucoseValue].self, forKey: .predictedGlucose),
  314. automaticDoseRecommendation: try container.decodeIfPresent(AutomaticDoseRecommendation.self, forKey: .automaticDoseRecommendation),
  315. manualBolusRecommendation: try container.decodeIfPresent(ManualBolusRecommendationWithDate.self, forKey: .manualBolusRecommendation),
  316. manualBolusRequested: try container.decodeIfPresent(Double.self, forKey: .manualBolusRequested),
  317. warnings: try container.decodeIfPresent([Issue].self, forKey: .warnings) ?? [],
  318. errors: try container.decodeIfPresent([Issue].self, forKey: .errors) ?? [],
  319. syncIdentifier: try container.decode(UUID.self, forKey: .syncIdentifier))
  320. }
  321. public func encode(to encoder: Encoder) throws {
  322. var container = encoder.container(keyedBy: CodingKeys.self)
  323. try container.encode(date, forKey: .date)
  324. try container.encode(controllerTimeZone, forKey: .controllerTimeZone)
  325. try container.encode(reason, forKey: .reason)
  326. try container.encodeIfPresent(settings, forKey: .settings)
  327. try container.encodeIfPresent(scheduleOverride, forKey: .scheduleOverride)
  328. try container.encodeIfPresent(controllerStatus, forKey: .controllerStatus)
  329. try container.encodeIfPresent(pumpManagerStatus, forKey: .pumpManagerStatus)
  330. try container.encodeIfPresent(pumpStatusHighlight, forKey: .pumpStatusHighlight)
  331. try container.encodeIfPresent(cgmManagerStatus, forKey: .cgmManagerStatus)
  332. try container.encodeIfPresent(lastReservoirValue, forKey: .lastReservoirValue)
  333. try container.encodeIfPresent(historicalGlucose, forKey: .historicalGlucose)
  334. try container.encodeIfPresent(originalCarbEntry, forKey: .originalCarbEntry)
  335. try container.encodeIfPresent(carbEntry, forKey: .carbEntry)
  336. try container.encodeIfPresent(manualGlucoseSample, forKey: .manualGlucoseSample)
  337. try container.encodeIfPresent(carbsOnBoard, forKey: .carbsOnBoard)
  338. try container.encodeIfPresent(insulinOnBoard, forKey: .insulinOnBoard)
  339. try container.encodeIfPresent(glucoseTargetRangeSchedule, forKey: .glucoseTargetRangeSchedule)
  340. try container.encodeIfPresent(predictedGlucose, forKey: .predictedGlucose)
  341. try container.encodeIfPresent(automaticDoseRecommendation, forKey: .automaticDoseRecommendation)
  342. try container.encodeIfPresent(manualBolusRecommendation, forKey: .manualBolusRecommendation)
  343. try container.encodeIfPresent(manualBolusRequested, forKey: .manualBolusRequested)
  344. try container.encodeIfPresent(!warnings.isEmpty ? warnings : nil, forKey: .warnings)
  345. try container.encodeIfPresent(!errors.isEmpty ? errors : nil, forKey: .errors)
  346. try container.encode(syncIdentifier, forKey: .syncIdentifier)
  347. }
  348. private enum CodingKeys: String, CodingKey {
  349. case date
  350. case controllerTimeZone
  351. case reason
  352. case settings
  353. case scheduleOverride
  354. case controllerStatus
  355. case pumpManagerStatus
  356. case pumpStatusHighlight
  357. case cgmManagerStatus
  358. case lastReservoirValue
  359. case historicalGlucose
  360. case originalCarbEntry
  361. case carbEntry
  362. case manualGlucoseSample
  363. case carbsOnBoard
  364. case insulinOnBoard
  365. case glucoseTargetRangeSchedule
  366. case predictedGlucose
  367. case automaticDoseRecommendation
  368. case manualBolusRecommendation
  369. case manualBolusRequested
  370. case warnings
  371. case errors
  372. case syncIdentifier
  373. }
  374. }
  375. // MARK: - Critical Event Log Export
  376. extension DosingDecisionStore: CriticalEventLog {
  377. private var exportProgressUnitCountPerObject: Int64 { 33 }
  378. private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
  379. public var exportName: String { "DosingDecisions.json" }
  380. public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
  381. var result: Result<Int64, Error>?
  382. self.store.managedObjectContext.performAndWait {
  383. do {
  384. let request: NSFetchRequest<DosingDecisionObject> = DosingDecisionObject.fetchRequest()
  385. request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
  386. let objectCount = try self.store.managedObjectContext.count(for: request)
  387. result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
  388. } catch let error {
  389. result = .failure(error)
  390. }
  391. }
  392. return result!
  393. }
  394. public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? {
  395. let encoder = JSONStreamEncoder(stream: stream)
  396. var modificationCounter: Int64 = 0
  397. var fetching = true
  398. var error: Error?
  399. while fetching && error == nil {
  400. self.store.managedObjectContext.performAndWait {
  401. do {
  402. guard !progress.isCancelled else {
  403. throw CriticalEventLogError.cancelled
  404. }
  405. let request: NSFetchRequest<DosingDecisionObject> = DosingDecisionObject.fetchRequest()
  406. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "modificationCounter > %d", modificationCounter),
  407. self.exportDatePredicate(startDate: startDate, endDate: endDate)])
  408. request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  409. request.fetchLimit = self.exportFetchLimit
  410. let objects = try self.store.managedObjectContext.fetch(request)
  411. if objects.isEmpty {
  412. fetching = false
  413. return
  414. }
  415. try encoder.encode(objects)
  416. modificationCounter = objects.last!.modificationCounter
  417. progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
  418. } catch let fetchError {
  419. error = fetchError
  420. }
  421. }
  422. }
  423. if let closeError = encoder.close(), error == nil {
  424. error = closeError
  425. }
  426. return error
  427. }
  428. private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
  429. var predicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  430. if let endDate = endDate {
  431. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "date < %@", endDate as NSDate)])
  432. }
  433. return predicate
  434. }
  435. }
  436. // MARK: - Core Data (Bulk) - TEST ONLY
  437. extension DosingDecisionStore {
  438. public func addStoredDosingDecisions(dosingDecisions: [StoredDosingDecision], completion: @escaping (Error?) -> Void) {
  439. guard !dosingDecisions.isEmpty else {
  440. completion(nil)
  441. return
  442. }
  443. dataAccessQueue.async {
  444. var error: Error?
  445. self.store.managedObjectContext.performAndWait {
  446. for dosingDecision in dosingDecisions {
  447. guard let data = self.encodeDosingDecision(dosingDecision) else {
  448. continue
  449. }
  450. let object = DosingDecisionObject(context: self.store.managedObjectContext)
  451. object.data = data
  452. object.date = dosingDecision.date
  453. }
  454. error = self.store.save()
  455. }
  456. guard error == nil else {
  457. completion(error)
  458. return
  459. }
  460. self.log.info("Added %d DosingDecisionObjects", dosingDecisions.count)
  461. self.delegate?.dosingDecisionStoreHasUpdatedDosingDecisionData(self)
  462. completion(nil)
  463. }
  464. }
  465. }