DoseStore.swift 74 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669
  1. //
  2. // DoseStore.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 1/27/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import CoreData
  9. import HealthKit
  10. import os.log
  11. public protocol DoseStoreDelegate: AnyObject {
  12. /**
  13. Informs the delegate that the dose store has updated pump event data.
  14. - Parameter doseStore: The dose store that has updated pump event data.
  15. */
  16. func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore)
  17. }
  18. public enum DoseStoreResult<T> {
  19. case success(T)
  20. case failure(DoseStore.DoseStoreError)
  21. }
  22. /**
  23. Manages storage, retrieval, and calculation of insulin pump delivery data.
  24. Pump data are stored in the following tiers:
  25. * In-memory cache, used for IOB and insulin effect calculation
  26. ```
  27. 0 [1.5 * insulinActionDuration]
  28. |––––––––––––––––––––—————————––|
  29. ```
  30. * On-disk Core Data store, unprotected
  31. ```
  32. 0 [24 hours]
  33. |––––––––––––––––––––––—————————|
  34. ```
  35. * HealthKit data, managed by the current application and persisted indefinitely
  36. ```
  37. 0
  38. |––––––––––––––––––––––——————————————>
  39. ```
  40. Private members should be assumed to not be thread-safe, and access should be contained to within blocks submitted to `persistenceStore.managedObjectContext`, which executes them on a private, serial queue.
  41. */
  42. public final class DoseStore {
  43. /// Notification posted when data was modifed.
  44. public static let valuesDidChange = NSNotification.Name(rawValue: "com.loopkit.DoseStore.valuesDidChange")
  45. public enum DoseStoreError: Error {
  46. case configurationError
  47. case initializationError(description: String, recoverySuggestion: String?)
  48. case persistenceError(description: String, recoverySuggestion: String?)
  49. case fetchError(description: String, recoverySuggestion: String?)
  50. init?(error: PersistenceController.PersistenceControllerError?) {
  51. if let error = error {
  52. self = .persistenceError(description: String(describing: error), recoverySuggestion: error.recoverySuggestion)
  53. } else {
  54. return nil
  55. }
  56. }
  57. }
  58. public weak var delegate: DoseStoreDelegate?
  59. private let log = OSLog(category: "DoseStore")
  60. public var longestEffectDuration: TimeInterval
  61. public var insulinModelProvider: InsulinModelProvider {
  62. get {
  63. return lockedInsulinModelProvider.value
  64. }
  65. set {
  66. lockedInsulinModelProvider.value = newValue
  67. persistenceController.managedObjectContext.perform {
  68. self.pumpEventQueryAfterDate = max(self.pumpEventQueryAfterDate, self.cacheStartDate)
  69. self.validateReservoirContinuity()
  70. }
  71. }
  72. }
  73. private let lockedInsulinModelProvider: Locked<InsulinModelProvider>
  74. /// A history of recently applied schedule overrides.
  75. private let overrideHistory: TemporaryScheduleOverrideHistory?
  76. public var basalProfile: BasalRateSchedule? {
  77. get {
  78. return lockedBasalProfile.value
  79. }
  80. set {
  81. lockedBasalProfile.value = newValue
  82. persistenceController.managedObjectContext.perform {
  83. self.clearReservoirNormalizedDoseCache()
  84. }
  85. }
  86. }
  87. private let lockedBasalProfile: Locked<BasalRateSchedule?>
  88. /// The basal profile, applying recent overrides relative to the current moment in time.
  89. public var basalProfileApplyingOverrideHistory: BasalRateSchedule? {
  90. if let basalProfile = basalProfile {
  91. return overrideHistory?.resolvingRecentBasalSchedule(basalProfile) ?? basalProfile
  92. } else {
  93. return nil
  94. }
  95. }
  96. public var insulinSensitivitySchedule: InsulinSensitivitySchedule? {
  97. get {
  98. return lockedInsulinSensitivitySchedule.value
  99. }
  100. set {
  101. lockedInsulinSensitivitySchedule.value = newValue
  102. }
  103. }
  104. private let lockedInsulinSensitivitySchedule: Locked<InsulinSensitivitySchedule?>
  105. /// The insulin sensitivity schedule, applying recent overrides relative to the current moment in time.
  106. public var insulinSensitivityScheduleApplyingOverrideHistory: InsulinSensitivitySchedule? {
  107. if let insulinSensitivitySchedule = insulinSensitivitySchedule {
  108. return overrideHistory?.resolvingRecentInsulinSensitivitySchedule(insulinSensitivitySchedule)
  109. } else {
  110. return nil
  111. }
  112. }
  113. /// The computed EGP schedule based on the basal profile and insulin sensitivity schedule.
  114. public var egpSchedule: EGPSchedule? {
  115. guard let basalProfile = basalProfile, let insulinSensitivitySchedule = insulinSensitivitySchedule else {
  116. return nil
  117. }
  118. return .egpSchedule(basalSchedule: basalProfile, insulinSensitivitySchedule: insulinSensitivitySchedule)
  119. }
  120. public let insulinDeliveryStore: InsulinDeliveryStore
  121. /// The HealthKit sample type managed by this store
  122. public var sampleType: HKSampleType {
  123. return insulinDeliveryStore.sampleType
  124. }
  125. /// True if the store requires authorization
  126. public var authorizationRequired: Bool {
  127. return insulinDeliveryStore.authorizationRequired
  128. }
  129. /// True if the user has explicitly denied access to any required share types
  130. public var sharingDenied: Bool {
  131. return insulinDeliveryStore.sharingDenied
  132. }
  133. /// The representation of the insulin pump for Health storage
  134. public var device: HKDevice? {
  135. get {
  136. return lockedDevice.value
  137. }
  138. set {
  139. lockedDevice.value = newValue
  140. }
  141. }
  142. private let lockedDevice = Locked<HKDevice?>(nil)
  143. /// Whether the pump generates events indicating the start of a scheduled basal rate after it had been interrupted.
  144. public var pumpRecordsBasalProfileStartEvents: Bool = false
  145. /// The sync version used for new samples written to HealthKit
  146. /// Choose a lower or higher sync version if the same sample might be written twice (e.g. from an extension and from an app) for deterministic conflict resolution
  147. public let syncVersion: Int
  148. /// Window for retrieving historical doses that might be used to reconcile current events
  149. private let pumpEventReconciliationWindow = TimeInterval(hours: 24)
  150. // MARK: -
  151. /// Initializes and configures a new store
  152. ///
  153. /// - Parameters:
  154. /// - healthStore: The HealthKit store for reading & writing insulin delivery
  155. /// - observeHealthKitSamplesFromOtherApps: Whether or not this Store should read HealthKit data written by other apps
  156. /// - storeSamplesToHealthKit: Whether or not this Store should store samples in HealthKit
  157. /// - cacheStore: The cache store for reading & writing short-term intermediate data
  158. /// - observationEnabled: Whether the store should observe changes from HealthKit
  159. /// - cacheLength: Maximum age of data to keep in the store.
  160. /// - insulinModelProvider: A factory for producing insulin models based on insulin type
  161. /// - longestEffectDuration: This determines the oldest age of doses to be retrieved for calculating glucose effects
  162. /// - basalProfile: The daily schedule of basal insulin rates
  163. /// - insulinSensitivitySchedule: The daily schedule of insulin sensitivity (ISF)
  164. /// - overrideHistory: A history of overrides to be used when calculating glucose effects
  165. /// - syncVersion: A version number for determining resolution in de-duplication
  166. /// - lastPumpEventsReconciliation: The date the PumpManger last reconciled with the pump
  167. /// - provenanceIdentifier: An id to store with new doses, indicating the provenance of the dose, usually the app's bundle identifier.
  168. /// - test_currentDate: Used for testing to mock current time
  169. public init(
  170. healthStore: HKHealthStore,
  171. observeHealthKitSamplesFromOtherApps: Bool = true,
  172. storeSamplesToHealthKit: Bool = true,
  173. cacheStore: PersistenceController,
  174. observationEnabled: Bool = true,
  175. cacheLength: TimeInterval = 24 /* hours */ * 60 /* minutes */ * 60 /* seconds */,
  176. insulinModelProvider: InsulinModelProvider,
  177. longestEffectDuration: TimeInterval,
  178. basalProfile: BasalRateSchedule?,
  179. insulinSensitivitySchedule: InsulinSensitivitySchedule?,
  180. overrideHistory: TemporaryScheduleOverrideHistory? = nil,
  181. syncVersion: Int = 1,
  182. lastPumpEventsReconciliation: Date? = nil,
  183. provenanceIdentifier: String,
  184. test_currentDate: Date? = nil
  185. ) {
  186. self.insulinDeliveryStore = InsulinDeliveryStore(
  187. healthStore: healthStore,
  188. observeHealthKitSamplesFromOtherApps: observeHealthKitSamplesFromOtherApps,
  189. storeSamplesToHealthKit: storeSamplesToHealthKit,
  190. cacheStore: cacheStore,
  191. observationEnabled: observationEnabled,
  192. cacheLength: cacheLength,
  193. provenanceIdentifier: provenanceIdentifier,
  194. test_currentDate: test_currentDate
  195. )
  196. self.lockedInsulinSensitivitySchedule = Locked(insulinSensitivitySchedule)
  197. self.lockedInsulinModelProvider = Locked(insulinModelProvider)
  198. self.longestEffectDuration = longestEffectDuration
  199. self.lockedBasalProfile = Locked(basalProfile)
  200. self.overrideHistory = overrideHistory
  201. self.persistenceController = cacheStore
  202. self.cacheLength = cacheLength
  203. self.syncVersion = syncVersion
  204. self.lockedLastPumpEventsReconciliation = Locked(lastPumpEventsReconciliation)
  205. self.pumpEventQueryAfterDate = cacheStartDate
  206. persistenceController.onReady { (error) -> Void in
  207. guard error == nil else {
  208. return
  209. }
  210. self.persistenceController.managedObjectContext.perform {
  211. // Find the newest PumpEvent date we have
  212. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  213. request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
  214. request.predicate = NSPredicate(format: "mutable != true")
  215. request.fetchLimit = 1
  216. if let events = try? self.persistenceController.managedObjectContext.fetch(request), let lastEvent = events.first {
  217. self.pumpEventQueryAfterDate = lastEvent.date
  218. }
  219. // Validate the state of the stored reservoir data.
  220. self.validateReservoirContinuity()
  221. }
  222. }
  223. }
  224. /// Clears all pump data from the on-disk store.
  225. ///
  226. /// Calling this method may result in data loss, as there is no check to ensure data has been synced first.
  227. ///
  228. /// - Parameter completion: A closure to call after the reset has completed
  229. public func resetPumpData(completion: ((_ error: DoseStoreError?) -> Void)? = nil) {
  230. log.info("Resetting all cached pump data")
  231. deleteAllPumpEvents { (error) in
  232. self.deleteAllReservoirValues { (error) in
  233. completion?(error)
  234. }
  235. }
  236. }
  237. private let persistenceController: PersistenceController
  238. private let cacheLength: TimeInterval
  239. private var purgeableValuesPredicate: NSPredicate {
  240. return NSPredicate(format: "date < %@", cacheStartDate as NSDate)
  241. }
  242. /// The maximum length of time to keep data around.
  243. /// Dose data is unprotected on disk, and should only remain persisted long enough to support dosing algorithms and until its persisted by the delegate.
  244. public var cacheStartDate: Date {
  245. return currentDate(timeIntervalSinceNow: -cacheLength)
  246. }
  247. private var recentStartDate: Date {
  248. return Calendar.current.date(byAdding: .day, value: -1, to: currentDate())!
  249. }
  250. internal func currentDate(timeIntervalSinceNow: TimeInterval = 0) -> Date {
  251. return insulinDeliveryStore.currentDate(timeIntervalSinceNow: timeIntervalSinceNow)
  252. }
  253. // MARK: - Reservoir Data
  254. /// The last-created reservoir object.
  255. private var lastStoredReservoirValue: StoredReservoirValue? {
  256. get {
  257. return lockedLastStoredReservoirValue.value
  258. }
  259. set {
  260. lockedLastStoredReservoirValue.value = newValue
  261. }
  262. }
  263. private let lockedLastStoredReservoirValue = Locked<StoredReservoirValue?>(nil)
  264. // The last-saved reservoir value
  265. public var lastReservoirValue: ReservoirValue? {
  266. return lastStoredReservoirValue
  267. }
  268. /// An incremental cache of temp basal doses based on reservoir records, used to avoid repeated work.
  269. ///
  270. /// *Access should be isolated to a managed object context block*
  271. private var recentReservoirNormalizedDoseEntriesCache: [DoseEntry]?
  272. /**
  273. *This method should only be called from within a managed object context block.*
  274. */
  275. private func clearReservoirNormalizedDoseCache() {
  276. recentReservoirNormalizedDoseEntriesCache = nil
  277. }
  278. /// Whether the current recent state of the stored reservoir data is considered
  279. /// continuous and reliable for the derivation of insulin effects
  280. ///
  281. /// *Access should be isolated to a managed object context block*
  282. private var areReservoirValuesValid = false
  283. // MARK: - Pump Event Data
  284. /// The earliest event date that should included in subsequent queries for pump event data.
  285. public private(set) var pumpEventQueryAfterDate: Date {
  286. get {
  287. return lockedPumpEventQueryAfterDate.value
  288. }
  289. set {
  290. lockedPumpEventQueryAfterDate.value = newValue
  291. }
  292. }
  293. private let lockedPumpEventQueryAfterDate = Locked<Date>(.distantPast)
  294. /// The last time the PumpManager reconciled events with the pump.
  295. public private(set) var lastPumpEventsReconciliation: Date? {
  296. get {
  297. return lockedLastPumpEventsReconciliation.value
  298. }
  299. set {
  300. lockedLastPumpEventsReconciliation.value = newValue
  301. }
  302. }
  303. private let lockedLastPumpEventsReconciliation: Locked<Date?>
  304. public var lastAddedPumpData: Date {
  305. return [lastReservoirValue?.startDate, lastPumpEventsReconciliation].compactMap { $0 }.max() ?? .distantPast
  306. }
  307. /// The date of the most recent pump prime event, if known.
  308. ///
  309. /// *Access should be isolated to a managed object context block*
  310. private var lastRecordedPrimeEventDate: Date? {
  311. get {
  312. if _lastRecordedPrimeEventDate == nil {
  313. if let pumpEvents = try? self.getPumpEventObjects(
  314. matching: NSPredicate(format: "type = %@", PumpEventType.prime.rawValue),
  315. chronological: false,
  316. limit: 1
  317. ),
  318. let firstEvent = pumpEvents.first
  319. {
  320. _lastRecordedPrimeEventDate = firstEvent.date
  321. } else {
  322. _lastRecordedPrimeEventDate = .distantPast
  323. }
  324. }
  325. return _lastRecordedPrimeEventDate
  326. }
  327. set {
  328. _lastRecordedPrimeEventDate = newValue
  329. }
  330. }
  331. private var _lastRecordedPrimeEventDate: Date?
  332. }
  333. // MARK: - Reservoir Operations
  334. extension DoseStore {
  335. /// Validates the current reservoir data for reliability in glucose effect calculation at the specified date
  336. ///
  337. /// *This method should only be called from within a managed object context block.*
  338. ///
  339. /// - Parameter date: The date to base the continuity calculation on. Defaults to now.
  340. /// - Returns: The array of reservoir data used in the calculation
  341. @discardableResult
  342. private func validateReservoirContinuity(at date: Date? = nil) -> [Reservoir] {
  343. let date = date ?? currentDate()
  344. // Consider any entries longer than 30 minutes, or with a value of 0, to be unreliable
  345. let maximumInterval = TimeInterval(minutes: 30)
  346. let continuityStartDate = date.addingTimeInterval(-longestEffectDuration)
  347. if let recentReservoirObjects = try? self.getReservoirObjects(since: continuityStartDate - maximumInterval),
  348. let oldestRelevantReservoirObject = recentReservoirObjects.last
  349. {
  350. // Verify reservoir timestamps are continuous
  351. let areReservoirValuesContinuous = recentReservoirObjects.reversed().isContinuous(
  352. from: continuityStartDate,
  353. to: date,
  354. within: maximumInterval
  355. )
  356. // also make sure prime events don't exist withing the insulin action duration
  357. let primeEventExistsWithinInsulinActionDuration = (lastRecordedPrimeEventDate ?? .distantPast) >= oldestRelevantReservoirObject.startDate
  358. self.areReservoirValuesValid = areReservoirValuesContinuous && !primeEventExistsWithinInsulinActionDuration
  359. self.lastStoredReservoirValue = recentReservoirObjects.first?.storedReservoirValue
  360. return recentReservoirObjects
  361. }
  362. self.areReservoirValuesValid = false
  363. self.lastStoredReservoirValue = nil
  364. return []
  365. }
  366. /**
  367. Adds and persists a new reservoir value
  368. - parameter unitVolume: The reservoir volume, in units
  369. - parameter date: The date of the volume reading
  370. - parameter completion: A closure called after the value was saved. This closure takes three arguments:
  371. - value: The new reservoir value, if it was saved
  372. - previousValue: The last new reservoir value
  373. - areStoredValuesContinous: Whether the current recent state of the stored reservoir data is considered continuous and reliable for deriving insulin effects after addition of this new value.
  374. - error: An error object explaining why the value could not be saved
  375. */
  376. public func addReservoirValue(_ unitVolume: Double, at date: Date, completion: @escaping (_ value: ReservoirValue?, _ previousValue: ReservoirValue?, _ areStoredValuesContinuous: Bool, _ error: DoseStoreError?) -> Void) {
  377. persistenceController.managedObjectContext.perform {
  378. // Perform some sanity checking of the new value against the most recent value.
  379. if let previousValue = self.lastReservoirValue {
  380. let isOutOfOrder = previousValue.endDate > date
  381. let isSameDate = previousValue.endDate == date
  382. let isConflicting = isSameDate && previousValue.unitVolume != unitVolume
  383. if isOutOfOrder || isConflicting {
  384. self.log.error("Added inconsistent reservoir value of %{public}.3fU at %{public}@ after %{public}.3fU at %{public}@. Resetting.", unitVolume, String(describing: date), previousValue.unitVolume, String(describing: previousValue.endDate))
  385. // If we're violating consistency of the previous value, reset.
  386. do {
  387. try self.purgeReservoirObjects()
  388. self.clearReservoirNormalizedDoseCache()
  389. self.validateReservoirContinuity()
  390. } catch let error {
  391. self.log.error("Error purging reservoir objects: %{public}@", String(describing: error))
  392. completion(nil, nil, false, DoseStoreError(error: error as? PersistenceController.PersistenceControllerError))
  393. return
  394. }
  395. // If no error on purge, continue with creation
  396. } else if isSameDate && previousValue.unitVolume == unitVolume {
  397. // Ignore duplicate adds
  398. self.log.error("Added duplicate reservoir value at %{public}@", String(describing: date))
  399. completion(nil, previousValue, self.areReservoirValuesValid, nil)
  400. return
  401. }
  402. }
  403. let reservoir = Reservoir(context: self.persistenceController.managedObjectContext)
  404. reservoir.volume = unitVolume
  405. reservoir.date = date
  406. let previousValue = self.lastStoredReservoirValue
  407. if let basalProfile = self.basalProfileApplyingOverrideHistory {
  408. var newValues: [StoredReservoirValue] = []
  409. if let previousValue = previousValue {
  410. newValues.append(previousValue)
  411. }
  412. newValues.append(reservoir.storedReservoirValue)
  413. let newDoseEntries = newValues.doseEntries
  414. if self.recentReservoirNormalizedDoseEntriesCache != nil {
  415. self.recentReservoirNormalizedDoseEntriesCache = self.recentReservoirNormalizedDoseEntriesCache!.filterDateRange(self.recentStartDate, nil)
  416. self.recentReservoirNormalizedDoseEntriesCache! += newDoseEntries.annotated(with: basalProfile)
  417. }
  418. }
  419. // Remove reservoir objects older than our cache length
  420. try? self.purgeReservoirObjects(matching: self.purgeableValuesPredicate)
  421. // Trigger a re-evaluation of continuity and update self.lastStoredReservoirValue
  422. self.validateReservoirContinuity()
  423. self.persistenceController.save { (error) -> Void in
  424. var saveError: DoseStoreError?
  425. if let error = error {
  426. saveError = DoseStoreError(error: error)
  427. }
  428. completion(
  429. reservoir.storedReservoirValue,
  430. previousValue,
  431. self.areReservoirValuesValid,
  432. saveError
  433. )
  434. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  435. }
  436. }
  437. }
  438. /// Retrieves reservoir values since the given date.
  439. ///
  440. /// - Parameters:
  441. /// - startDate: The earliest reservoir record date to include
  442. /// - limit: An optional limit to the number of values returned
  443. /// - completion: A closure called after retrieval
  444. /// - result: An array of reservoir values in reverse-chronological order
  445. public func getReservoirValues(since startDate: Date, limit: Int? = nil, completion: @escaping (_ result: DoseStoreResult<[ReservoirValue]>) -> Void) {
  446. persistenceController.managedObjectContext.perform {
  447. do {
  448. let values = try self.getReservoirObjects(since: startDate, limit: limit).map { $0.storedReservoirValue }
  449. completion(.success(values))
  450. } catch let error as DoseStoreError {
  451. completion(.failure(error))
  452. } catch {
  453. assertionFailure()
  454. }
  455. }
  456. }
  457. /// *This method should only be called from within a managed object context block.*
  458. ///
  459. /// - Parameters:
  460. /// - startDate: The earliest reservoir record date to include
  461. /// - limit: An optional limit to the number of objects returned
  462. /// - Returns: An array of reservoir managed objects, in reverse-chronological order
  463. /// - Throws: An error describing the failure to fetch objects
  464. private func getReservoirObjects(since startDate: Date, limit: Int? = nil) throws -> [Reservoir] {
  465. let request: NSFetchRequest<Reservoir> = Reservoir.fetchRequest()
  466. request.predicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  467. request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
  468. if let limit = limit {
  469. request.fetchLimit = limit
  470. }
  471. do {
  472. return try persistenceController.managedObjectContext.fetch(request)
  473. } catch let fetchError as NSError {
  474. throw DoseStoreError.fetchError(description: fetchError.localizedDescription, recoverySuggestion: fetchError.localizedRecoverySuggestion)
  475. }
  476. }
  477. /// Retrieves normalized dose values derived from reservoir readings
  478. ///
  479. /// *This method should only be called from within a managed object context block.*
  480. ///
  481. /// - Parameters:
  482. /// - start: The earliest date of entries to include
  483. /// - end: The latest date of entries to include, defaulting to the distant future.
  484. /// - Returns: An array of normalized entries
  485. /// - Throws: A DoseStoreError describing a failure
  486. private func getNormalizedReservoirDoseEntries(start: Date, end: Date? = nil) throws -> [DoseEntry] {
  487. if let normalizedDoses = self.recentReservoirNormalizedDoseEntriesCache, let firstDoseDate = normalizedDoses.first?.startDate, firstDoseDate <= start {
  488. return normalizedDoses.filterDateRange(start, end)
  489. } else {
  490. guard let basalProfile = self.basalProfileApplyingOverrideHistory else {
  491. throw DoseStoreError.configurationError
  492. }
  493. let doses = try self.getReservoirObjects(since: start).reversed().doseEntries
  494. let normalizedDoses = doses.annotated(with: basalProfile)
  495. self.recentReservoirNormalizedDoseEntriesCache = normalizedDoses
  496. return normalizedDoses.filterDateRange(start, end)
  497. }
  498. }
  499. /**
  500. Deletes a persisted reservoir value
  501. - parameter value: The value to delete
  502. - parameter completion: A closure called after the value was deleted. This closure takes two arguments:
  503. - parameter deletedValues: An array of removed values
  504. - parameter error: An error object explaining why the value could not be deleted
  505. */
  506. public func deleteReservoirValue(_ value: ReservoirValue, completion: @escaping (_ deletedValues: [ReservoirValue], _ error: DoseStoreError?) -> Void) {
  507. persistenceController.managedObjectContext.perform {
  508. var deletedObjects = [ReservoirValue]()
  509. if let storedValue = value as? StoredReservoirValue,
  510. let objectID = self.persistenceController.managedObjectContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: storedValue.objectIDURL),
  511. let object = try? self.persistenceController.managedObjectContext.existingObject(with: objectID)
  512. {
  513. self.persistenceController.managedObjectContext.delete(object)
  514. deletedObjects.append(storedValue)
  515. self.validateReservoirContinuity()
  516. }
  517. self.persistenceController.save { (error) in
  518. self.clearReservoirNormalizedDoseCache()
  519. completion(deletedObjects, DoseStoreError(error: error))
  520. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  521. }
  522. }
  523. }
  524. /// Deletes all persisted reservoir values
  525. ///
  526. /// - Parameter completion: A closure called after all the values are deleted. This closure takes a single argument:
  527. /// - Parameter error: An error explaining why the deletion failed
  528. public func deleteAllReservoirValues(_ completion: @escaping (_ error: DoseStoreError?) -> Void) {
  529. persistenceController.managedObjectContext.perform {
  530. do {
  531. self.log.info("Deleting all reservoir values")
  532. try self.purgeReservoirObjects()
  533. self.persistenceController.save { (error) in
  534. self.clearReservoirNormalizedDoseCache()
  535. self.validateReservoirContinuity()
  536. completion(DoseStoreError(error: error))
  537. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  538. }
  539. } catch let error as PersistenceController.PersistenceControllerError {
  540. completion(DoseStoreError(error: error))
  541. } catch {
  542. assertionFailure()
  543. }
  544. }
  545. }
  546. /**
  547. Removes reservoir objects older than the recency predicate, and re-evaluates the continuity of the remaining objects
  548. *This method should only be called from within a managed object context block.*
  549. - throws: PersistenceController.PersistenceControllerError.coreDataError if the delete request failed
  550. */
  551. private func purgeReservoirObjects(matching predicate: NSPredicate? = nil) throws {
  552. let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: Reservoir.entity().name!)
  553. fetchRequest.predicate = predicate
  554. let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
  555. deleteRequest.resultType = .resultTypeObjectIDs
  556. do {
  557. if let result = try persistenceController.managedObjectContext.execute(deleteRequest) as? NSBatchDeleteResult,
  558. let objectIDs = result.result as? [NSManagedObjectID],
  559. objectIDs.count > 0
  560. {
  561. let changes = [NSDeletedObjectsKey: objectIDs]
  562. NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [persistenceController.managedObjectContext])
  563. self.validateReservoirContinuity()
  564. }
  565. } catch let error as NSError {
  566. throw PersistenceController.PersistenceControllerError.coreDataError(error)
  567. }
  568. }
  569. }
  570. // MARK: - Pump Event Operations
  571. extension DoseStore {
  572. /**
  573. Adds and persists new pump events.
  574. Events are deduplicated by a unique constraint on `NewPumpEvent.getter:raw`.
  575. - parameter events: An array of new pump events. Pump events should have end times reflective of when delivery is actually expected to be finished, as doses that end prior to a reservoir reading are ignored when reservoir data is being used.
  576. - parameter lastReconciliation: The date that pump events were most recently reconciled against recorded pump history. Pump events are assumed to be reflective of delivery up until this point in time. If reservoir values are recorded after this time, they may be used to supplement event based delivery.
  577. - parameter completion: A closure called after the events are saved. The closure takes a single argument:
  578. - parameter error: An error object explaining why the events could not be saved.
  579. */
  580. public func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStoreError?) -> Void) {
  581. lastPumpEventsReconciliation = lastReconciliation
  582. guard events.count > 0 else {
  583. completion(nil)
  584. return
  585. }
  586. for event in events {
  587. if let dose = event.dose {
  588. self.log.debug("Add %@, isMutable=%@", String(describing: dose), String(describing: event.dose?.isMutable))
  589. }
  590. }
  591. persistenceController.managedObjectContext.perform {
  592. var lastFinalDate: Date?
  593. var firstMutableDate: Date?
  594. var primeValueAdded = false
  595. // Remove any stored mutable pumpEvents; any that are still valid should be included in events
  596. do {
  597. try self.purgePumpEventObjects(matching: NSPredicate(format: "mutable == YES"))
  598. } catch let error {
  599. completion(DoseStoreError(error: .coreDataError(error as NSError)))
  600. return
  601. }
  602. // Remove old doses
  603. self.purgePumpEventObjects(before: self.cacheStartDate, completion: { error in
  604. if let error = error {
  605. self.log.error("Error purging PumpEvent objects: %{public}@", String(describing: error))
  606. }
  607. })
  608. // There is no guarantee of event ordering, so we must search the entire array to find key date boundaries.
  609. for event in events {
  610. if case .prime? = event.type {
  611. primeValueAdded = true
  612. }
  613. let isMutable = event.dose?.isMutable == true
  614. let wasProgrammedByPumpUI = event.dose?.wasProgrammedByPumpUI ?? false
  615. if isMutable {
  616. firstMutableDate = min(event.date, firstMutableDate ?? event.date)
  617. } else {
  618. lastFinalDate = max(event.date, lastFinalDate ?? event.date)
  619. }
  620. let object = PumpEvent(context: self.persistenceController.managedObjectContext)
  621. object.date = event.date
  622. object.raw = event.raw
  623. object.title = event.title
  624. object.type = event.type
  625. object.mutable = isMutable
  626. object.dose = event.dose
  627. object.alarmType = event.alarmType
  628. object.wasProgrammedByPumpUI = wasProgrammedByPumpUI
  629. }
  630. // Only change pumpEventQueryAfterDate if we received new finalized records.
  631. if let finalDate = lastFinalDate {
  632. if let mutableDate = firstMutableDate, mutableDate < finalDate {
  633. self.pumpEventQueryAfterDate = mutableDate
  634. } else {
  635. self.pumpEventQueryAfterDate = finalDate
  636. }
  637. }
  638. if primeValueAdded {
  639. self.lastRecordedPrimeEventDate = nil
  640. self.validateReservoirContinuity()
  641. }
  642. self.persistenceController.save { (error) -> Void in
  643. self.syncPumpEventsToInsulinDeliveryStore(resolveMutable: true) { _ in
  644. completion(DoseStoreError(error: error))
  645. self.delegate?.doseStoreHasUpdatedPumpEventData(self)
  646. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  647. }
  648. }
  649. }
  650. }
  651. public func deletePumpEvent(_ event: PersistedPumpEvent, completion: @escaping (_ error: DoseStoreError?) -> Void) {
  652. persistenceController.managedObjectContext.perform {
  653. if let objectID = self.persistenceController.managedObjectContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: event.objectIDURL),
  654. let object = try? self.persistenceController.managedObjectContext.existingObject(with: objectID)
  655. {
  656. self.persistenceController.managedObjectContext.delete(object)
  657. }
  658. // Reset the latest query date to the newest PumpEvent
  659. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  660. request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
  661. request.predicate = NSPredicate(format: "mutable != true")
  662. request.fetchLimit = 1
  663. if let events = try? self.persistenceController.managedObjectContext.fetch(request),
  664. let lastEvent = events.first
  665. {
  666. self.pumpEventQueryAfterDate = lastEvent.date
  667. } else {
  668. self.pumpEventQueryAfterDate = self.cacheStartDate
  669. }
  670. self.persistenceController.save { (error) in
  671. completion(DoseStoreError(error: error))
  672. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  673. self.lastRecordedPrimeEventDate = nil
  674. self.validateReservoirContinuity()
  675. }
  676. }
  677. }
  678. /// Deletes all persisted pump events
  679. ///
  680. /// - Parameter completion: A closure called after all the events are deleted. This closure takes a single argument:
  681. /// - Parameter error: An error explaining why the deletion failed
  682. public func deleteAllPumpEvents(_ completion: @escaping (_ error: DoseStoreError?) -> Void) {
  683. syncPumpEventsToInsulinDeliveryStore { (error) in
  684. if let error = error {
  685. self.log.error("Error performing final sync to insulin delivery store before deleteAllPumpEvents: %{public}@", String(describing: error))
  686. }
  687. self.persistenceController.managedObjectContext.perform {
  688. do {
  689. self.log.info("Deleting all pump events")
  690. try self.purgePumpEventObjects()
  691. self.persistenceController.save { (error) in
  692. self.pumpEventQueryAfterDate = self.cacheStartDate
  693. self.lastPumpEventsReconciliation = nil
  694. self.lastRecordedPrimeEventDate = nil
  695. completion(DoseStoreError(error: error))
  696. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  697. }
  698. } catch let error as PersistenceController.PersistenceControllerError {
  699. completion(DoseStoreError(error: error))
  700. } catch {
  701. assertionFailure()
  702. }
  703. }
  704. }
  705. }
  706. /**
  707. Adds and persists doses. Doses *cannot* be mutable.
  708. - parameter doses: An array of dose entries to add.
  709. - parameter completion: A closure called after the doses are saved. The closure takes a single argument:
  710. - parameter error: An error object explaining why the doses could not be saved.
  711. */
  712. public func addDoses(_ doses: [DoseEntry], from device: HKDevice?, completion: @escaping (_ error: Error?) -> Void) {
  713. assert(!doses.contains(where: { $0.isMutable }))
  714. guard doses.count > 0 else {
  715. completion(nil)
  716. return
  717. }
  718. self.persistenceController.save { (error) -> Void in
  719. if let error = error {
  720. self.log.error("Error saving: %{public}@", String(describing: error))
  721. }
  722. self.insulinDeliveryStore.addDoseEntries(doses, from: device, syncVersion: self.syncVersion) { (result) in
  723. switch result {
  724. case .success:
  725. completion(nil)
  726. self.syncPumpEventsToInsulinDeliveryStore { error in
  727. completion(error)
  728. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  729. }
  730. case .failure(let error):
  731. self.log.error("Error adding dose: %{public}@", String(describing: error))
  732. completion(error)
  733. }
  734. }
  735. }
  736. }
  737. /// Deletes one particular manually entered dose from the store
  738. ///
  739. /// - Parameter dose: Dose to delete.
  740. /// - Parameter completion: A closure called after the event deleted. This closure takes a single argument:
  741. /// - Parameter success: True if dose was successfully deleted
  742. public func deleteDose(_ dose: DoseEntry, completion: @escaping (_ error: DoseStoreError?) -> Void) {
  743. guard let syncIdentifier = dose.syncIdentifier else {
  744. self.log.error("Unable to delete PersistedManualEntryDose: no syncIdentifier")
  745. completion(DoseStoreError.fetchError(description: "Unable to delete dose: syncIdentifier is missing", recoverySuggestion: "File an issue report in Github"))
  746. return
  747. }
  748. insulinDeliveryStore.deleteDose(bySyncIdentifier: syncIdentifier) { (error) in
  749. if let error = error {
  750. completion(DoseStoreError.persistenceError(description: error, recoverySuggestion: nil))
  751. } else {
  752. completion(nil)
  753. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  754. }
  755. }
  756. }
  757. /// Deletes all manually entered doses
  758. ///
  759. /// - Parameter completion: A closure called after all the events are deleted. This closure takes a single argument:
  760. /// - Parameter error: An error explaining why the deletion failed
  761. public func deleteAllManuallyEnteredDoses(since startDate: Date, _ completion: @escaping (_ error: DoseStoreError?) -> Void) {
  762. self.log.info("Deleting all manually entered doses since %{public}@", String(describing: startDate))
  763. insulinDeliveryStore.deleteAllManuallyEnteredDoses(since: startDate) { error in
  764. completion(error)
  765. NotificationCenter.default.post(name: DoseStore.valuesDidChange, object: self)
  766. }
  767. }
  768. /// Attempts to store doses from pump events to insulin delivery store
  769. private func syncPumpEventsToInsulinDeliveryStore(after start: Date? = nil, resolveMutable: Bool = false, completion: @escaping (_ error: Error?) -> Void) {
  770. insulinDeliveryStore.getLastImmutableBasalEndDate { (result) in
  771. switch result {
  772. case .success(let date):
  773. // Limit the query behavior to 24 hours
  774. let date = max(date, self.recentStartDate)
  775. self.savePumpEventsToInsulinDeliveryStore(after: start ?? date, resolveMutable: resolveMutable, completion: completion)
  776. case .failure(let error):
  777. completion(error)
  778. }
  779. }
  780. }
  781. /// Processes and saves dose events on or after the given date to insulin delivery store
  782. ///
  783. /// - Parameters:
  784. /// - start: The date on and after which to include doses
  785. /// - resolveMutable: Resolve mutable dose entries during saving
  786. /// - completion: A closure called on completion
  787. /// - error: An error if one ocurred during processing or saving
  788. private func savePumpEventsToInsulinDeliveryStore(after start: Date, resolveMutable: Bool, completion: @escaping (_ error: Error?) -> Void) {
  789. getPumpEventDoseEntriesForSavingToInsulinDeliveryStore(startingAt: start) { (result) in
  790. switch result {
  791. case .success(let doses):
  792. guard doses.count > 0 else {
  793. completion(nil)
  794. return
  795. }
  796. for dose in doses {
  797. self.log.debug("Adding dose to insulin delivery store: %@", String(describing: dose))
  798. }
  799. self.insulinDeliveryStore.addDoseEntries(doses, from: self.device, syncVersion: self.syncVersion, resolveMutable: resolveMutable) { (result) in
  800. switch result {
  801. case .success:
  802. completion(nil)
  803. case .failure(let error):
  804. self.log.error("Error adding doses: %{public}@", String(describing: error))
  805. completion(error)
  806. }
  807. }
  808. case .failure(let error):
  809. completion(error)
  810. }
  811. }
  812. }
  813. /// Fetches a timeline of doses, filling in gaps between delivery changes with the scheduled basal delivery
  814. /// if the pump doesn't already handle this
  815. ///
  816. /// - Parameters:
  817. /// - start: The date on and after which to include doses
  818. /// - completion: A closure called on completion
  819. /// - result: The doses along with schedule basal
  820. private func getPumpEventDoseEntriesForSavingToInsulinDeliveryStore(startingAt: Date, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) {
  821. // Can't store to insulin delivery store if we don't know end of reconciled range, or if we already have doses after the end
  822. guard let endingAt = lastPumpEventsReconciliation, endingAt > startingAt else {
  823. completion(.success([]))
  824. return
  825. }
  826. self.persistenceController.managedObjectContext.perform {
  827. let doses: [DoseEntry]
  828. do {
  829. doses = try self.getNormalizedPumpEventDoseEntriesForSavingToInsulinDeliveryStore(basalStart: startingAt, end: self.currentDate())
  830. } catch let error as DoseStoreError {
  831. self.log.error("Error while fetching doses to add to insulin delivery store: %{public}@", String(describing: error))
  832. completion(.failure(error))
  833. return
  834. } catch {
  835. assertionFailure()
  836. return
  837. }
  838. guard !doses.isEmpty else
  839. {
  840. completion(.success([]))
  841. return
  842. }
  843. guard let basalSchedule = self.basalProfileApplyingOverrideHistory else {
  844. self.log.error("Can't save %d doses to insulin delivery store because no basal profile is configured", doses.count)
  845. completion(.failure(DoseStoreError.configurationError))
  846. return
  847. }
  848. let reconciledDoses = doses.overlayBasalSchedule(basalSchedule, startingAt: startingAt, insertingBasalEntries: !self.pumpRecordsBasalProfileStartEvents)
  849. completion(.success(reconciledDoses))
  850. }
  851. }
  852. /// Fetches manually entered doses.
  853. ///
  854. /// - Parameter startDate: The earliest dose startDate to include
  855. /// - Returns: An array of manually entered dose managed objects, in reverse-chronological order, or an error describing the failure to fetch objects
  856. public func getManuallyEnteredDoses(since startDate: Date, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) {
  857. insulinDeliveryStore.getManuallyEnteredDoses(since: startDate, chronological: false, completion: completion)
  858. }
  859. /// Retrieves pump event values since the given date.
  860. ///
  861. /// - Parameters:
  862. /// - startDate: The earliest pump event date to include
  863. /// - completion: A closure called after retrieval
  864. /// - result: An array of pump event values in reverse-chronological order
  865. public func getPumpEventValues(since startDate: Date, completion: @escaping (_ result: DoseStoreResult<[PersistedPumpEvent]>) -> Void) {
  866. persistenceController.managedObjectContext.perform {
  867. do {
  868. let events = try self.getPumpEventObjects(since: startDate).map { $0.persistedPumpEvent }
  869. completion(.success(events))
  870. } catch let error as DoseStoreError {
  871. completion(.failure(error))
  872. } catch {
  873. assertionFailure()
  874. }
  875. }
  876. }
  877. /// *This method should only be called from within a managed object context block.*
  878. ///
  879. /// - Parameter startDate: The earliest pump event date to include
  880. /// - Returns: An array of pump event managed objects, in reverse-chronological order
  881. /// - Throws: An error describing the failure to fetch objects
  882. private func getPumpEventObjects(since startDate: Date) throws -> [PumpEvent] {
  883. return try getPumpEventObjects(
  884. matching: NSPredicate(format: "date >= %@", startDate as NSDate),
  885. chronological: false
  886. )
  887. }
  888. /// *This method should only be called from within a managed object context block.*
  889. ///
  890. /// Objects are ordered by date using the DoseType sort ordering as a tiebreaker for stability
  891. ///
  892. /// - Parameters:
  893. /// - predicate: The predicate to apply to the objects
  894. /// - chronological: Whether to return the objects in chronological or reverse-chronological order
  895. /// - Returns: An array of pump events in the specified order by date
  896. /// - Throws: An error describing the failure to fetch objects
  897. private func getPumpEventObjects(matching predicate: NSPredicate, chronological: Bool, limit: Int? = nil) throws -> [PumpEvent] {
  898. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  899. request.predicate = predicate
  900. request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: chronological)]
  901. if let limit = limit {
  902. request.fetchLimit = limit
  903. }
  904. do {
  905. return try persistenceController.managedObjectContext.fetch(request).sorted(by: { (lhs, rhs) -> Bool in
  906. let (first, second) = chronological ? (lhs, rhs) : (rhs, lhs)
  907. if first.startDate == second.startDate,
  908. let firstType = first.type, let secondType = second.type
  909. {
  910. return firstType.sortOrder < secondType.sortOrder
  911. } else {
  912. return first.startDate < second.startDate
  913. }
  914. })
  915. } catch let fetchError as NSError {
  916. throw DoseStoreError.fetchError(description: fetchError.localizedDescription, recoverySuggestion: fetchError.localizedRecoverySuggestion)
  917. }
  918. }
  919. /// *This method should only be called from within a managed object context block.*
  920. ///
  921. /// - Parameters:
  922. /// - start: The earliest dose end date to include
  923. /// - end: The latest dose start date to include
  924. /// - Returns: An array of doses from pump events
  925. /// - Throws: An error describing the failure to fetch objects
  926. private func getNormalizedPumpEventDoseEntries(start: Date, end: Date? = nil) throws -> [DoseEntry] {
  927. guard let basalProfile = self.basalProfileApplyingOverrideHistory else {
  928. throw DoseStoreError.configurationError
  929. }
  930. let queryStart = start.addingTimeInterval(-pumpEventReconciliationWindow)
  931. let doses = try getPumpEventObjects(
  932. matching: NSPredicate(format: "date >= %@ && doseType != nil", queryStart as NSDate),
  933. chronological: true
  934. ).compactMap({ $0.dose })
  935. let normalizedDoses = doses.reconciled().annotated(with: basalProfile)
  936. return normalizedDoses.filterDateRange(start, end)
  937. }
  938. /// *This method should only be called from within a managed object context block.*
  939. ///
  940. /// - Returns: An array of doses from pump events that were marked mutable
  941. /// - Throws: An error describing the failure to fetch objects
  942. private func getNormalizedMutablePumpEventDoseEntries(start: Date) throws -> [DoseEntry] {
  943. guard let basalProfile = self.basalProfileApplyingOverrideHistory else {
  944. throw DoseStoreError.configurationError
  945. }
  946. let doses = try getPumpEventObjects(
  947. matching: NSPredicate(format: "mutable == true && doseType != nil"),
  948. chronological: true
  949. ).compactMap({ $0.dose })
  950. let normalizedDoses = doses.filterDateRange(start, nil).reconciled().annotated(with: basalProfile)
  951. return normalizedDoses.map { $0.trimmed(from: start) }
  952. }
  953. /// *This method should only be called from within a managed object context block.*
  954. ///
  955. /// - Parameters:
  956. /// - basalStart: The earliest basal dose start date to include
  957. /// - end: The latest dose end date to include
  958. /// - Returns: An array of doses from pump events
  959. /// - Throws: An error describing the failure to fetch objects
  960. private func getNormalizedPumpEventDoseEntriesForSavingToInsulinDeliveryStore(basalStart: Date, end: Date) throws -> [DoseEntry] {
  961. guard let basalProfile = self.basalProfileApplyingOverrideHistory else {
  962. throw DoseStoreError.configurationError
  963. }
  964. self.log.info("Fetching Pumpevents between %{public}@ and %{public}@ for saving to InsulinDeliveryStore", String(describing: basalStart), String(describing: end))
  965. // Make sure we look far back enough to have prior temp basal records to reconcile
  966. // resumption of temp basal after suspend/resume.
  967. let queryStart = basalStart.addingTimeInterval(-pumpEventReconciliationWindow)
  968. let afterBasalStart = NSPredicate(format: "date >= %@ && doseType != nil", queryStart as NSDate)
  969. let allBoluses = NSPredicate(format: "date >= %@ && doseType == %@", recentStartDate as NSDate, DoseType.bolus.rawValue)
  970. let doses = try getPumpEventObjects(
  971. matching: NSCompoundPredicate(orPredicateWithSubpredicates: [afterBasalStart, allBoluses]),
  972. chronological: true
  973. ).compactMap({ $0.dose })
  974. // Ignore any doses which have not yet ended by the specified date.
  975. // Also, since we are retrieving dosing history older than basalStart for
  976. // reconciliation purposes, we need to filter that out after reconciliation.
  977. let normalizedDoses = doses.reconciled().filter({ $0.endDate <= end || $0.isMutable }).annotated(with: basalProfile).filter({ $0.startDate >= basalStart || $0.type == .bolus })
  978. return normalizedDoses
  979. }
  980. public func purgePumpEventObjects(before date: Date, completion: (Error?) -> Void) {
  981. do {
  982. let count = try purgePumpEventObjects(matching: NSPredicate(format: "date < %@", date as NSDate))
  983. self.log.info("Purged %d PumpEvents", count)
  984. completion(nil)
  985. } catch let error {
  986. self.log.error("Unable to purge PumpEvents: %{public}@", String(describing: error))
  987. completion(error)
  988. }
  989. }
  990. /**
  991. Removes uploaded pump event objects older than the recency predicate
  992. *This method should only be called from within a managed object context block.*
  993. - throws: A core data exception if the delete request failed
  994. */
  995. @discardableResult
  996. private func purgePumpEventObjects(matching predicate: NSPredicate? = nil) throws -> Int {
  997. let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: PumpEvent.entity().name!)
  998. fetchRequest.predicate = predicate
  999. let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
  1000. deleteRequest.resultType = .resultTypeObjectIDs
  1001. if let result = try persistenceController.managedObjectContext.execute(deleteRequest) as? NSBatchDeleteResult,
  1002. let objectIDs = result.result as? [NSManagedObjectID],
  1003. objectIDs.count > 0
  1004. {
  1005. let changes = [NSDeletedObjectsKey: objectIDs]
  1006. NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [persistenceController.managedObjectContext])
  1007. persistenceController.managedObjectContext.refreshAllObjects()
  1008. return objectIDs.count
  1009. }
  1010. return 0
  1011. }
  1012. }
  1013. extension DoseStore {
  1014. /// Retrieves dose entries normalized to the current basal schedule, for visualization purposes.
  1015. ///
  1016. /// Doses are derived from pump events if they've been updated within the last 15 minutes or reservoir data is incomplete.
  1017. ///
  1018. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  1019. ///
  1020. /// - Parameters:
  1021. /// - start: The earliest endDate of entries to retrieve
  1022. /// - end: The latest startDate of entries to retrieve, if provided
  1023. /// - completion: A closure called once the entries have been retrieved
  1024. /// - result: An array of dose entries, in chronological order by startDate
  1025. public func getNormalizedDoseEntries(start: Date, end: Date? = nil, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) {
  1026. insulinDeliveryStore.getDoseEntries(start: start, end: end) { (result) in
  1027. switch result {
  1028. case .failure(let error):
  1029. completion(.failure(.persistenceError(description: error.localizedDescription, recoverySuggestion: nil)))
  1030. case .success(let insulinDeliveryDoses):
  1031. let filteredStart = max(self.lastPumpEventsReconciliation ?? start, start)
  1032. self.persistenceController.managedObjectContext.perform {
  1033. do {
  1034. let doses: [DoseEntry]
  1035. // Reservoir data is used only if it's continuous and the pumpmanager hasn't reconciled since the last reservoir reading
  1036. if self.areReservoirValuesValid, let reservoirEndDate = self.lastStoredReservoirValue?.startDate, reservoirEndDate > self.lastPumpEventsReconciliation ?? .distantPast {
  1037. let reservoirDoses = try self.getNormalizedReservoirDoseEntries(start: filteredStart, end: end)
  1038. let endOfReservoirData = self.lastStoredReservoirValue?.endDate ?? .distantPast
  1039. let mutableDoses = try self.getNormalizedMutablePumpEventDoseEntries(start: endOfReservoirData)
  1040. doses = insulinDeliveryDoses + reservoirDoses.map({ $0.trimmed(from: filteredStart) }) + mutableDoses
  1041. } else {
  1042. // Includes mutable doses.
  1043. doses = insulinDeliveryDoses.appendedUnion(with: try self.getNormalizedPumpEventDoseEntries(start: filteredStart, end: end))
  1044. }
  1045. completion(.success(doses))
  1046. } catch let error as DoseStoreError {
  1047. completion(.failure(error))
  1048. } catch {
  1049. assertionFailure()
  1050. }
  1051. }
  1052. }
  1053. }
  1054. }
  1055. /// Retrieves the maximum insulin on-board value from the two timeline values nearest to the specified date
  1056. ///
  1057. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  1058. ///
  1059. /// - Parameters:
  1060. /// - date: The date of the value to retrieve
  1061. /// - completion: A closure called once the value has been retrieved
  1062. /// - result: The insulin on-board value
  1063. public func insulinOnBoard(at date: Date, completion: @escaping (_ result: DoseStoreResult<InsulinValue>) -> Void) {
  1064. getInsulinOnBoardValues(start: date.addingTimeInterval(.minutes(-5)), end: date.addingTimeInterval(.minutes(5))) { (result) -> Void in
  1065. switch result {
  1066. case .failure(let error):
  1067. completion(.failure(error))
  1068. case .success(let values):
  1069. let closest = values.allElementsAdjacent(to: date)
  1070. // Return the larger of the two bounding values, for the scenario when a bolus
  1071. // was scheduled between the two values; we want to return the later, larger value
  1072. guard let maxValue = closest.max(by: { return $0.value < $1.value }) else {
  1073. // If we have no iob values in the store, and did not encounter an error, return 0
  1074. completion(.success(InsulinValue(startDate: date, value: 0)))
  1075. return
  1076. }
  1077. completion(.success(maxValue))
  1078. }
  1079. }
  1080. }
  1081. /// Retrieves a timeline of unabsorbed insulin values.
  1082. ///
  1083. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  1084. ///
  1085. /// - Parameters:
  1086. /// - start: The earliest date of values to retrieve
  1087. /// - end: The latest date of values to retrieve, if provided
  1088. /// - basalDosingEnd: The date at which continuing doses should be assumed to be cancelled
  1089. /// - completion: A closure called once the values have been retrieved
  1090. /// - result: An array of insulin values, in chronological order
  1091. public func getInsulinOnBoardValues(start: Date, end: Date? = nil, basalDosingEnd: Date? = nil, completion: @escaping (_ result: DoseStoreResult<[InsulinValue]>) -> Void) {
  1092. // To properly know IOB at startDate, we need to go back another DIA hours
  1093. let doseStart = start.addingTimeInterval(-longestEffectDuration)
  1094. getNormalizedDoseEntries(start: doseStart, end: end) { (result) in
  1095. switch result {
  1096. case .failure(let error):
  1097. completion(.failure(error))
  1098. case .success(let doses):
  1099. let trimmedDoses = doses.map { $0.trimmed(to: basalDosingEnd) }
  1100. let insulinOnBoard = trimmedDoses.insulinOnBoard(insulinModelProvider: self.insulinModelProvider, longestEffectDuration: self.longestEffectDuration)
  1101. completion(.success(insulinOnBoard.filterDateRange(start, end)))
  1102. }
  1103. }
  1104. }
  1105. /// Retrieves a timeline of effect on blood glucose from doses
  1106. ///
  1107. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  1108. ///
  1109. /// - Parameters:
  1110. /// - start: The earliest date of effects to retrieve
  1111. /// - end: The latest date of effects to retrieve, if provided
  1112. /// - basalDosingEnd: The date at which continuing doses should be assumed to be cancelled
  1113. /// - completion: A closure called once the effects have been retrieved
  1114. /// - result: An array of effects, in chronological order
  1115. public func getGlucoseEffects(start: Date, end: Date? = nil, basalDosingEnd: Date? = Date(), completion: @escaping (_ result: DoseStoreResult<[GlucoseEffect]>) -> Void) {
  1116. guard let insulinSensitivitySchedule = self.insulinSensitivityScheduleApplyingOverrideHistory else {
  1117. completion(.failure(.configurationError))
  1118. return
  1119. }
  1120. // To properly know glucose effects at startDate, we need to go back another DIA hours
  1121. let doseStart = start.addingTimeInterval(-longestEffectDuration)
  1122. getNormalizedDoseEntries(start: doseStart, end: end) { (result) in
  1123. switch result {
  1124. case .failure(let error):
  1125. completion(.failure(error))
  1126. case .success(let doses):
  1127. let trimmedDoses = doses.map { (dose) -> DoseEntry in
  1128. guard dose.type != .bolus else {
  1129. return dose
  1130. }
  1131. return dose.trimmed(to: basalDosingEnd)
  1132. }
  1133. let glucoseEffects = trimmedDoses.glucoseEffects(insulinModelProvider: self.insulinModelProvider, longestEffectDuration: self.longestEffectDuration, insulinSensitivity: insulinSensitivitySchedule)
  1134. completion(.success(glucoseEffects.filterDateRange(start, end)))
  1135. }
  1136. }
  1137. }
  1138. /// Retrieves the estimated total number of units delivered since the specified date.
  1139. ///
  1140. /// - Parameters:
  1141. /// - startDate: The date after which delivery should be calculated
  1142. /// - completion: A closure called once the total has been retrieved with arguments:
  1143. /// - result: The total units delivered and the date of the first dose
  1144. public func getTotalUnitsDelivered(since startDate: Date, completion: @escaping (_ result: DoseStoreResult<InsulinValue>) -> Void) {
  1145. persistenceController.managedObjectContext.perform {
  1146. self.getNormalizedDoseEntries(start: startDate) { (result) in
  1147. switch result {
  1148. case .success(let doses):
  1149. let trimmedDoses = doses.map { $0.trimmed(from: startDate, to: self.currentDate())}
  1150. let result = InsulinValue(
  1151. startDate: startDate,
  1152. value: trimmedDoses.totalDelivery
  1153. )
  1154. completion(.success(result))
  1155. case .failure(let error):
  1156. completion(.failure(error))
  1157. }
  1158. }
  1159. }
  1160. }
  1161. }
  1162. extension DoseStore {
  1163. /// Generates a diagnostic report about the current state
  1164. ///
  1165. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  1166. ///
  1167. /// - parameter completion: The closure takes a single argument of the report string.
  1168. public func generateDiagnosticReport(_ completion: @escaping (_ report: String) -> Void) {
  1169. var report: [String] = [
  1170. "## DoseStore",
  1171. "",
  1172. "* insulinModelProvider: \(String(reflecting: insulinModelProvider))",
  1173. "* basalProfile: \(basalProfile?.debugDescription ?? "")",
  1174. "* basalProfileApplyingOverrideHistory \(basalProfileApplyingOverrideHistory?.debugDescription ?? "nil")",
  1175. "* insulinSensitivitySchedule: \(insulinSensitivitySchedule?.debugDescription ?? "")",
  1176. "* insulinSensitivityScheduleApplyingOverrideHistory \(insulinSensitivityScheduleApplyingOverrideHistory?.debugDescription ?? "nil")",
  1177. "* overrideHistory: \(overrideHistory.map(String.init(describing:)) ?? "nil")",
  1178. "* egpSchedule: \(egpSchedule?.debugDescription ?? "nil")",
  1179. "* areReservoirValuesValid: \(areReservoirValuesValid)",
  1180. "* lastPumpEventsReconciliation: \(String(describing: lastPumpEventsReconciliation))",
  1181. "* lastStoredReservoirValue: \(String(describing: lastStoredReservoirValue))",
  1182. "* pumpEventQueryAfterDate: \(pumpEventQueryAfterDate)",
  1183. "* lastRecordedPrimeEventDate: \(String(describing: lastRecordedPrimeEventDate))",
  1184. "* pumpRecordsBasalProfileStartEvents: \(pumpRecordsBasalProfileStartEvents)",
  1185. "* device: \(String(describing: device))",
  1186. ]
  1187. insulinOnBoard(at: currentDate()) { (result) in
  1188. report.append("")
  1189. switch result {
  1190. case .failure(let error):
  1191. report.append("* insulinOnBoard: \(error)")
  1192. case .success(let value):
  1193. report.append("* insulinOnBoard: \(String(describing: value))")
  1194. }
  1195. self.getReservoirValues(since: Date.distantPast) { (result) in
  1196. report.append("")
  1197. report.append("### getReservoirValues")
  1198. switch result {
  1199. case .failure(let error):
  1200. report.append("Error: \(error)")
  1201. case .success(let values):
  1202. report.append("")
  1203. report.append("* Reservoir(startDate, unitVolume)")
  1204. for value in values {
  1205. report.append("* \(value.startDate), \(value.unitVolume)")
  1206. }
  1207. }
  1208. self.getPumpEventValues(since: Date.distantPast) { (result) in
  1209. report.append("")
  1210. report.append("### getPumpEventValues")
  1211. var firstPumpEventDate = self.cacheStartDate
  1212. switch result {
  1213. case .failure(let error):
  1214. report.append("Error: \(error)")
  1215. case .success(let values):
  1216. report.append("")
  1217. if let firstEvent = values.last {
  1218. firstPumpEventDate = firstEvent.date
  1219. }
  1220. for value in values {
  1221. report.append("* \(value)")
  1222. }
  1223. }
  1224. self.getNormalizedDoseEntries(start: firstPumpEventDate) { (result) in
  1225. report.append("")
  1226. report.append("### getNormalizedDoseEntries")
  1227. switch result {
  1228. case .failure(let error):
  1229. report.append("Error: \(error)")
  1230. case .success(let entries):
  1231. report.append("")
  1232. for entry in entries {
  1233. report.append("* \(entry)")
  1234. }
  1235. }
  1236. self.getPumpEventDoseEntriesForSavingToInsulinDeliveryStore(startingAt: firstPumpEventDate, completion: { (result) in
  1237. report.append("")
  1238. report.append("### getNormalizedPumpEventDoseEntriesOverlaidWithBasalEntries")
  1239. switch result {
  1240. case .failure(let error):
  1241. report.append("Error: \(error)")
  1242. case .success(let entries):
  1243. report.append("")
  1244. for entry in entries {
  1245. report.append("* \(entry)")
  1246. }
  1247. }
  1248. self.getManuallyEnteredDoses(since: firstPumpEventDate) { (result) in
  1249. report.append("")
  1250. report.append("### getManuallyEnteredDoses")
  1251. switch result {
  1252. case .failure(let error):
  1253. report.append("Error: \(error)")
  1254. case .success(let entries):
  1255. report.append("")
  1256. for entry in entries {
  1257. report.append("* \(entry)")
  1258. }
  1259. }
  1260. self.insulinDeliveryStore.generateDiagnosticReport { (result) in
  1261. report.append("")
  1262. report.append(result)
  1263. report.append("")
  1264. completion(report.joined(separator: "\n"))
  1265. }
  1266. }
  1267. })
  1268. }
  1269. }
  1270. }
  1271. }
  1272. }
  1273. }
  1274. extension DoseStore {
  1275. public struct QueryAnchor: Equatable, RawRepresentable {
  1276. public typealias RawValue = [String: Any]
  1277. internal var modificationCounter: Int64
  1278. public init() {
  1279. self.modificationCounter = 0
  1280. }
  1281. public init?(rawValue: RawValue) {
  1282. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  1283. return nil
  1284. }
  1285. self.modificationCounter = modificationCounter
  1286. }
  1287. public var rawValue: RawValue {
  1288. var rawValue: RawValue = [:]
  1289. rawValue["modificationCounter"] = modificationCounter
  1290. return rawValue
  1291. }
  1292. }
  1293. public enum PumpEventQueryResult {
  1294. case success(QueryAnchor, [PersistedPumpEvent])
  1295. case failure(Error)
  1296. }
  1297. public func executePumpEventQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (PumpEventQueryResult) -> Void) {
  1298. var queryAnchor = queryAnchor ?? QueryAnchor()
  1299. var queryResult = [PersistedPumpEvent]()
  1300. var queryError: Error?
  1301. guard limit > 0 else {
  1302. completion(.success(queryAnchor, []))
  1303. return
  1304. }
  1305. persistenceController.managedObjectContext.performAndWait {
  1306. let storedRequest: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  1307. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  1308. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  1309. storedRequest.fetchLimit = limit
  1310. do {
  1311. let stored = try self.persistenceController.managedObjectContext.fetch(storedRequest)
  1312. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  1313. queryAnchor.modificationCounter = modificationCounter
  1314. }
  1315. queryResult.append(contentsOf: stored.compactMap { $0.persistedPumpEvent })
  1316. } catch let error {
  1317. queryError = error
  1318. }
  1319. }
  1320. if let queryError = queryError {
  1321. completion(.failure(queryError))
  1322. return
  1323. }
  1324. completion(.success(queryAnchor, queryResult))
  1325. }
  1326. }
  1327. // MARK: - Critical Event Log Export
  1328. extension DoseStore: CriticalEventLog {
  1329. private var exportProgressUnitCountPerObject: Int64 { 1 }
  1330. private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
  1331. public var exportName: String { "Doses.json" }
  1332. public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
  1333. var result: Result<Int64, Error>?
  1334. self.persistenceController.managedObjectContext.performAndWait {
  1335. do {
  1336. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  1337. request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
  1338. let objectCount = try self.persistenceController.managedObjectContext.count(for: request)
  1339. result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
  1340. } catch let error {
  1341. result = .failure(error)
  1342. }
  1343. }
  1344. return result!
  1345. }
  1346. public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? {
  1347. let encoder = JSONStreamEncoder(stream: stream)
  1348. var modificationCounter: Int64 = 0
  1349. var fetching = true
  1350. var error: Error?
  1351. while fetching && error == nil {
  1352. self.persistenceController.managedObjectContext.performAndWait {
  1353. do {
  1354. guard !progress.isCancelled else {
  1355. throw CriticalEventLogError.cancelled
  1356. }
  1357. let request: NSFetchRequest<PumpEvent> = PumpEvent.fetchRequest()
  1358. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "modificationCounter > %d", modificationCounter),
  1359. self.exportDatePredicate(startDate: startDate, endDate: endDate)])
  1360. request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  1361. request.fetchLimit = self.exportFetchLimit
  1362. let objects = try self.persistenceController.managedObjectContext.fetch(request)
  1363. if objects.isEmpty {
  1364. fetching = false
  1365. return
  1366. }
  1367. try encoder.encode(objects)
  1368. modificationCounter = objects.last!.modificationCounter
  1369. progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
  1370. } catch let fetchError {
  1371. error = fetchError
  1372. }
  1373. }
  1374. }
  1375. if let closeError = encoder.close(), error == nil {
  1376. error = closeError
  1377. }
  1378. return error
  1379. }
  1380. private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
  1381. var predicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  1382. if let endDate = endDate {
  1383. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "date < %@", endDate as NSDate)])
  1384. }
  1385. return predicate
  1386. }
  1387. }
  1388. // MARK: - Core Data (Bulk) - TEST ONLY
  1389. extension DoseStore {
  1390. public func addPumpEvents(events: [PersistedPumpEvent]) -> Error? {
  1391. guard !events.isEmpty, !events.contains(where: { $0.dose?.isMutable == true }) else {
  1392. return nil
  1393. }
  1394. var error: Error?
  1395. let dispatchGroup = DispatchGroup()
  1396. dispatchGroup.enter()
  1397. self.persistenceController.managedObjectContext.perform {
  1398. for event in events {
  1399. let object = PumpEvent(context: self.persistenceController.managedObjectContext)
  1400. object.update(from: event)
  1401. }
  1402. self.persistenceController.save { saveError in
  1403. guard saveError == nil else {
  1404. error = saveError
  1405. dispatchGroup.leave()
  1406. return
  1407. }
  1408. self.syncPumpEventsToInsulinDeliveryStore(after: events.compactMap { $0.date }.min()) { syncError in
  1409. error = syncError
  1410. dispatchGroup.leave()
  1411. }
  1412. }
  1413. }
  1414. dispatchGroup.wait()
  1415. guard error == nil else {
  1416. return error
  1417. }
  1418. self.log.info("Added %d PumpEvents", events.count)
  1419. self.delegate?.doseStoreHasUpdatedPumpEventData(self)
  1420. return nil
  1421. }
  1422. }