PumpHistoryStorage.swift 14 KB

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