PumpHistoryStorage.swift 20 KB

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