PumpHistoryStorage.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import CoreData
  2. import Foundation
  3. import LoopKit
  4. import SwiftDate
  5. import Swinject
  6. protocol PumpHistoryDelegate: AnyObject {
  7. /*
  8. Informs the delegate that the Carbs Storage has updated Carbs
  9. */
  10. func pumpHistoryHasUpdated(_ pumpHistoryStorage: BasePumpHistoryStorage)
  11. }
  12. protocol PumpHistoryObserver {
  13. func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent])
  14. }
  15. protocol PumpHistoryStorage {
  16. var delegate: PumpHistoryDelegate? { get set }
  17. func storePumpEvents(_ events: [NewPumpEvent])
  18. func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
  19. func recent() -> [PumpHistoryEvent]
  20. func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  21. func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
  22. func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
  23. func deleteInsulin(at date: Date)
  24. }
  25. final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
  26. private let processQueue = DispatchQueue(label: "BasePumpHistoryStorage.processQueue")
  27. @Injected() private var storage: FileStorage!
  28. @Injected() private var broadcaster: Broadcaster!
  29. @Injected() private var settings: SettingsManager!
  30. public weak var delegate: PumpHistoryDelegate?
  31. init(resolver: Resolver) {
  32. injectServices(resolver)
  33. }
  34. typealias PumpEvent = PumpEventStored.EventType
  35. typealias TempType = PumpEventStored.TempType
  36. private let context = CoreDataStack.shared.newTaskContext()
  37. private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
  38. let roundedValue = (dose / increment).rounded() * increment
  39. return Decimal(roundedValue)
  40. }
  41. func storePumpEvents(_ events: [NewPumpEvent]) {
  42. processQueue.async {
  43. self.context.perform {
  44. for event in events {
  45. // Fetch to filter out duplicates
  46. // TODO: - move this to the Core Data Class
  47. let existingEvents: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
  48. ofType: PumpEventStored.self,
  49. onContext: self.context,
  50. predicate: NSPredicate.duplicateInLastHour(event.date),
  51. key: "timestamp",
  52. ascending: false,
  53. batchSize: 50
  54. )
  55. switch event.type {
  56. case .bolus:
  57. guard let dose = event.dose else { continue }
  58. let amount = self.roundDose(
  59. dose.unitsInDeliverableIncrements,
  60. toIncrement: Double(self.settings.preferences.bolusIncrement)
  61. )
  62. guard existingEvents.isEmpty else {
  63. // Duplicate found, do not store the event
  64. print("Duplicate event found with timestamp: \(event.date)")
  65. if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
  66. if existingEvent.timestamp == event.date {
  67. if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
  68. // Update existing event with new smaller value
  69. existingEvent.bolus?.amount = amount as NSDecimalNumber
  70. existingEvent.bolus?.isSMB = dose.automatic ?? true
  71. existingEvent.isUploadedToNS = false
  72. existingEvent.isUploadedToHealth = false
  73. existingEvent.isUploadedToTidepool = false
  74. print("Updated existing event with smaller value: \(amount)")
  75. }
  76. }
  77. }
  78. continue
  79. }
  80. let newPumpEvent = PumpEventStored(context: self.context)
  81. newPumpEvent.id = UUID().uuidString
  82. newPumpEvent.timestamp = event.date
  83. newPumpEvent.type = PumpEvent.bolus.rawValue
  84. newPumpEvent.isUploadedToNS = false
  85. newPumpEvent.isUploadedToHealth = false
  86. newPumpEvent.isUploadedToTidepool = false
  87. let newBolusEntry = BolusStored(context: self.context)
  88. newBolusEntry.pumpEvent = newPumpEvent
  89. newBolusEntry.amount = NSDecimalNumber(decimal: amount)
  90. newBolusEntry.isExternal = dose.manuallyEntered
  91. newBolusEntry.isSMB = dose.automatic ?? true
  92. case .tempBasal:
  93. guard let dose = event.dose else { continue }
  94. guard existingEvents.isEmpty else {
  95. // Duplicate found, do not store the event
  96. print("Duplicate event found with timestamp: \(event.date)")
  97. continue
  98. }
  99. let rate = Decimal(dose.unitsPerHour)
  100. let minutes = (dose.endDate - dose.startDate).timeInterval / 60
  101. let delivered = dose.deliveredUnits
  102. let date = event.date
  103. let isCancel = delivered != nil
  104. guard !isCancel else { continue }
  105. let newPumpEvent = PumpEventStored(context: self.context)
  106. newPumpEvent.id = UUID().uuidString
  107. newPumpEvent.timestamp = date
  108. newPumpEvent.type = PumpEvent.tempBasal.rawValue
  109. newPumpEvent.isUploadedToNS = false
  110. newPumpEvent.isUploadedToHealth = false
  111. let newTempBasal = TempBasalStored(context: self.context)
  112. newTempBasal.pumpEvent = newPumpEvent
  113. newTempBasal.duration = Int16(round(minutes))
  114. newTempBasal.rate = rate as NSDecimalNumber
  115. newTempBasal.tempType = TempType.absolute.rawValue
  116. case .suspend:
  117. guard existingEvents.isEmpty else {
  118. // Duplicate found, do not store the event
  119. print("Duplicate event found with timestamp: \(event.date)")
  120. continue
  121. }
  122. let newPumpEvent = PumpEventStored(context: self.context)
  123. newPumpEvent.id = UUID().uuidString
  124. newPumpEvent.timestamp = event.date
  125. newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
  126. newPumpEvent.isUploadedToNS = false
  127. newPumpEvent.isUploadedToHealth = false
  128. case .resume:
  129. guard existingEvents.isEmpty else {
  130. // Duplicate found, do not store the event
  131. print("Duplicate event found with timestamp: \(event.date)")
  132. continue
  133. }
  134. let newPumpEvent = PumpEventStored(context: self.context)
  135. newPumpEvent.id = UUID().uuidString
  136. newPumpEvent.timestamp = event.date
  137. newPumpEvent.type = PumpEvent.pumpResume.rawValue
  138. newPumpEvent.isUploadedToNS = false
  139. newPumpEvent.isUploadedToHealth = false
  140. case .rewind:
  141. guard existingEvents.isEmpty else {
  142. // Duplicate found, do not store the event
  143. print("Duplicate event found with timestamp: \(event.date)")
  144. continue
  145. }
  146. let newPumpEvent = PumpEventStored(context: self.context)
  147. newPumpEvent.id = UUID().uuidString
  148. newPumpEvent.timestamp = event.date
  149. newPumpEvent.type = PumpEvent.rewind.rawValue
  150. newPumpEvent.isUploadedToNS = false
  151. newPumpEvent.isUploadedToHealth = false
  152. case .prime:
  153. guard existingEvents.isEmpty else {
  154. // Duplicate found, do not store the event
  155. print("Duplicate event found with timestamp: \(event.date)")
  156. continue
  157. }
  158. let newPumpEvent = PumpEventStored(context: self.context)
  159. newPumpEvent.id = UUID().uuidString
  160. newPumpEvent.timestamp = event.date
  161. newPumpEvent.type = PumpEvent.prime.rawValue
  162. newPumpEvent.isUploadedToNS = false
  163. newPumpEvent.isUploadedToHealth = false
  164. case .alarm:
  165. guard existingEvents.isEmpty else {
  166. // Duplicate found, do not store the event
  167. print("Duplicate event found with timestamp: \(event.date)")
  168. continue
  169. }
  170. let newPumpEvent = PumpEventStored(context: self.context)
  171. newPumpEvent.id = UUID().uuidString
  172. newPumpEvent.timestamp = event.date
  173. newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
  174. newPumpEvent.isUploadedToNS = false
  175. newPumpEvent.isUploadedToHealth = false
  176. newPumpEvent.note = event.title
  177. default:
  178. continue
  179. }
  180. }
  181. do {
  182. guard self.context.hasChanges else { return }
  183. try self.context.save()
  184. self.delegate?.pumpHistoryHasUpdated(self)
  185. debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
  186. } catch let error as NSError {
  187. debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
  188. }
  189. }
  190. }
  191. }
  192. func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async {
  193. debug(.default, "External insulin saved")
  194. await context.perform {
  195. // create pump event
  196. let newPumpEvent = PumpEventStored(context: self.context)
  197. newPumpEvent.id = UUID().uuidString
  198. newPumpEvent.timestamp = timestamp
  199. newPumpEvent.type = PumpEvent.bolus.rawValue
  200. newPumpEvent.isUploadedToNS = false
  201. newPumpEvent.isUploadedToHealth = false
  202. // create bolus entry and specify relationship to pump event
  203. let newBolusEntry = BolusStored(context: self.context)
  204. newBolusEntry.pumpEvent = newPumpEvent
  205. newBolusEntry.amount = amount as NSDecimalNumber
  206. newBolusEntry.isExternal = true // we are creating an external dose
  207. newBolusEntry.isSMB = false // the dose is manually administered
  208. do {
  209. guard self.context.hasChanges else { return }
  210. try self.context.save()
  211. self.delegate?.pumpHistoryHasUpdated(self)
  212. } catch {
  213. print(error.localizedDescription)
  214. }
  215. }
  216. }
  217. func recent() -> [PumpHistoryEvent] {
  218. storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
  219. }
  220. func deleteInsulin(at date: Date) {
  221. processQueue.sync {
  222. var allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
  223. guard let entryIndex = allValues.firstIndex(where: { $0.timestamp == date }) else {
  224. return
  225. }
  226. allValues.remove(at: entryIndex)
  227. storage.save(allValues, as: OpenAPS.Monitor.pumpHistory)
  228. broadcaster.notify(PumpHistoryObserver.self, on: processQueue) {
  229. $0.pumpHistoryDidUpdate(allValues)
  230. }
  231. }
  232. }
  233. func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
  234. if event.bolus!.isSMB {
  235. return .smb
  236. }
  237. if event.bolus!.isExternal {
  238. return .isExternal
  239. }
  240. return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
  241. }
  242. func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  243. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  244. ofType: PumpEventStored.self,
  245. onContext: context,
  246. predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
  247. key: "timestamp",
  248. ascending: false,
  249. fetchLimit: 288
  250. )
  251. guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
  252. return await context.perform { [self] in
  253. fetchedPumpEvents.map { event in
  254. switch event.type {
  255. case PumpEvent.bolus.rawValue:
  256. // eventType determines whether bolus is external, smb or manual (=administered via app by user)
  257. let eventType = determineBolusEventType(for: event)
  258. return NightscoutTreatment(
  259. duration: nil,
  260. rawDuration: nil,
  261. rawRate: nil,
  262. absolute: nil,
  263. rate: nil,
  264. eventType: eventType,
  265. createdAt: event.timestamp,
  266. enteredBy: NightscoutTreatment.local,
  267. bolus: nil,
  268. insulin: event.bolus?.amount as Decimal?,
  269. notes: nil,
  270. carbs: nil,
  271. fat: nil,
  272. protein: nil,
  273. targetTop: nil,
  274. targetBottom: nil,
  275. id: event.id
  276. )
  277. case PumpEvent.tempBasal.rawValue:
  278. return NightscoutTreatment(
  279. duration: Int(event.tempBasal?.duration ?? 0),
  280. rawDuration: nil,
  281. rawRate: nil,
  282. absolute: event.tempBasal?.rate as Decimal?,
  283. rate: event.tempBasal?.rate as Decimal?,
  284. eventType: .nsTempBasal,
  285. createdAt: event.timestamp,
  286. enteredBy: NightscoutTreatment.local,
  287. bolus: nil,
  288. insulin: nil,
  289. notes: nil,
  290. carbs: nil,
  291. fat: nil,
  292. protein: nil,
  293. targetTop: nil,
  294. targetBottom: nil,
  295. id: event.id
  296. )
  297. case PumpEvent.pumpSuspend.rawValue:
  298. return NightscoutTreatment(
  299. duration: nil,
  300. rawDuration: nil,
  301. rawRate: nil,
  302. absolute: nil,
  303. rate: nil,
  304. eventType: .nsNote,
  305. createdAt: event.timestamp,
  306. enteredBy: NightscoutTreatment.local,
  307. bolus: nil,
  308. insulin: nil,
  309. notes: PumpEvent.pumpSuspend.rawValue,
  310. carbs: nil,
  311. fat: nil,
  312. protein: nil,
  313. targetTop: nil,
  314. targetBottom: nil
  315. )
  316. case PumpEvent.pumpResume.rawValue:
  317. return NightscoutTreatment(
  318. duration: nil,
  319. rawDuration: nil,
  320. rawRate: nil,
  321. absolute: nil,
  322. rate: nil,
  323. eventType: .nsNote,
  324. createdAt: event.timestamp,
  325. enteredBy: NightscoutTreatment.local,
  326. bolus: nil,
  327. insulin: nil,
  328. notes: PumpEvent.pumpResume.rawValue,
  329. carbs: nil,
  330. fat: nil,
  331. protein: nil,
  332. targetTop: nil,
  333. targetBottom: nil
  334. )
  335. case PumpEvent.rewind.rawValue:
  336. return NightscoutTreatment(
  337. duration: nil,
  338. rawDuration: nil,
  339. rawRate: nil,
  340. absolute: nil,
  341. rate: nil,
  342. eventType: .nsInsulinChange,
  343. createdAt: event.timestamp,
  344. enteredBy: NightscoutTreatment.local,
  345. bolus: nil,
  346. insulin: nil,
  347. notes: nil,
  348. carbs: nil,
  349. fat: nil,
  350. protein: nil,
  351. targetTop: nil,
  352. targetBottom: nil
  353. )
  354. case PumpEvent.prime.rawValue:
  355. return NightscoutTreatment(
  356. duration: nil,
  357. rawDuration: nil,
  358. rawRate: nil,
  359. absolute: nil,
  360. rate: nil,
  361. eventType: .nsSiteChange,
  362. createdAt: event.timestamp,
  363. enteredBy: NightscoutTreatment.local,
  364. bolus: nil,
  365. insulin: nil,
  366. notes: nil,
  367. carbs: nil,
  368. fat: nil,
  369. protein: nil,
  370. targetTop: nil,
  371. targetBottom: nil
  372. )
  373. case PumpEvent.pumpAlarm.rawValue:
  374. return NightscoutTreatment(
  375. duration: 30, // minutes
  376. rawDuration: nil,
  377. rawRate: nil,
  378. absolute: nil,
  379. rate: nil,
  380. eventType: .nsAnnouncement,
  381. createdAt: event.timestamp,
  382. enteredBy: NightscoutTreatment.local,
  383. bolus: nil,
  384. insulin: nil,
  385. notes: "Alarm \(String(describing: event.note)) \(PumpEvent.pumpAlarm.rawValue)",
  386. carbs: nil,
  387. fat: nil,
  388. protein: nil,
  389. targetTop: nil,
  390. targetBottom: nil
  391. )
  392. default:
  393. return nil
  394. }
  395. }.compactMap { $0 }
  396. }
  397. }
  398. func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent] {
  399. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  400. ofType: PumpEventStored.self,
  401. onContext: context,
  402. predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
  403. key: "timestamp",
  404. ascending: false,
  405. fetchLimit: 288
  406. )
  407. guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
  408. return await context.perform {
  409. fetchedPumpEvents.map { event in
  410. switch event.type {
  411. case PumpEvent.bolus.rawValue:
  412. return PumpHistoryEvent(
  413. id: event.id ?? UUID().uuidString,
  414. type: .bolus,
  415. timestamp: event.timestamp ?? Date(),
  416. amount: event.bolus?.amount as Decimal?
  417. )
  418. case PumpEvent.tempBasal.rawValue:
  419. return PumpHistoryEvent(
  420. id: event.id ?? UUID().uuidString,
  421. type: .tempBasal,
  422. timestamp: event.timestamp ?? Date(),
  423. amount: event.tempBasal?.rate as Decimal?
  424. )
  425. default:
  426. return nil
  427. }
  428. }.compactMap { $0 }
  429. }
  430. }
  431. func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent] {
  432. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  433. ofType: PumpEventStored.self,
  434. onContext: context,
  435. predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
  436. key: "timestamp",
  437. ascending: false,
  438. fetchLimit: 288
  439. )
  440. guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
  441. return await context.perform {
  442. fetchedPumpEvents.map { event in
  443. switch event.type {
  444. case PumpEvent.bolus.rawValue:
  445. return PumpHistoryEvent(
  446. id: event.id ?? UUID().uuidString,
  447. type: .bolus,
  448. timestamp: event.timestamp ?? Date(),
  449. amount: event.bolus?.amount as Decimal?,
  450. isSMB: event.bolus?.isSMB ?? true,
  451. isExternal: event.bolus?.isExternal ?? false
  452. )
  453. case PumpEvent.tempBasal.rawValue:
  454. return PumpHistoryEvent(
  455. id: event.id ?? UUID().uuidString,
  456. type: .tempBasal,
  457. timestamp: event.timestamp ?? Date(),
  458. amount: event.tempBasal?.rate as Decimal?
  459. )
  460. default:
  461. return nil
  462. }
  463. }.compactMap { $0 }
  464. }
  465. }
  466. }