PumpHistoryStorage.swift 18 KB

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