PumpHistoryStorage.swift 16 KB

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