PumpHistoryStorage.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import Foundation
  2. import LoopKit
  3. import SwiftDate
  4. import Swinject
  5. protocol PumpHistoryObserver {
  6. func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent])
  7. }
  8. protocol PumpHistoryStorage {
  9. func storePumpEvents(_ events: [NewPumpEvent])
  10. func storeEvents(_ events: [PumpHistoryEvent])
  11. func storeJournalCarbs(_ carbs: Int)
  12. func recent() -> [PumpHistoryEvent]
  13. func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
  14. func saveCancelTempEvents()
  15. func deleteInsulin(at date: Date)
  16. }
  17. final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
  18. private let processQueue = DispatchQueue(label: "BasePumpHistoryStorage.processQueue")
  19. @Injected() private var storage: FileStorage!
  20. @Injected() private var broadcaster: Broadcaster!
  21. init(resolver: Resolver) {
  22. injectServices(resolver)
  23. }
  24. typealias PumpEvent = PumpEventStored.EventType
  25. typealias TempType = PumpEventStored.TempType
  26. private let context = CoreDataStack.shared.backgroundContext
  27. func storePumpEvents(_ events: [NewPumpEvent]) {
  28. processQueue.async {
  29. let eventsToStore = events.flatMap { event -> [PumpHistoryEvent] in
  30. let id = event.raw.md5String
  31. switch event.type {
  32. case .bolus:
  33. guard let dose = event.dose else { return [] }
  34. let amount = Decimal(string: dose.unitsInDeliverableIncrements.description)
  35. let minutes = Int((dose.endDate - dose.startDate).timeInterval / 60)
  36. self.context.perform {
  37. // create pump event
  38. let newPumpEvent = PumpEventStored(context: self.context)
  39. newPumpEvent.id = id
  40. newPumpEvent.timestamp = event.date
  41. newPumpEvent.type = PumpEvent.bolus.rawValue
  42. // create bolus entry and specify relationship to pump event
  43. let newBolusEntry = BolusStored(context: self.context)
  44. newBolusEntry.pumpEvent = newPumpEvent
  45. newBolusEntry.amount = amount as? NSDecimalNumber
  46. newBolusEntry.isExternal = dose.manuallyEntered
  47. newBolusEntry.isSMB = dose.automatic ?? true
  48. // TODO: - do we need duration here?
  49. do {
  50. try CoreDataStack.shared.backgroundContext.saveContext()
  51. } catch {
  52. print(error.localizedDescription)
  53. }
  54. }
  55. return [PumpHistoryEvent(
  56. id: id,
  57. type: .bolus,
  58. timestamp: event.date,
  59. amount: amount,
  60. duration: minutes,
  61. durationMin: nil,
  62. rate: nil,
  63. temp: nil,
  64. carbInput: nil,
  65. isSMB: dose.automatic,
  66. isExternal: dose.manuallyEntered
  67. )]
  68. case .tempBasal:
  69. guard let dose = event.dose else { return [] }
  70. let rate = Decimal(dose.unitsPerHour)
  71. let minutes = (dose.endDate - dose.startDate).timeInterval / 60
  72. let delivered = dose.deliveredUnits
  73. let date = event.date
  74. let isCancel = delivered != nil //! event.isMutable && delivered != nil
  75. guard !isCancel else { return [] }
  76. self.context.perform {
  77. // create pump event
  78. let newPumpEvent = PumpEventStored(context: self.context)
  79. newPumpEvent.id = id
  80. newPumpEvent.timestamp = date
  81. newPumpEvent.type = PumpEvent.tempBasal.rawValue
  82. // create temp basal and specify relationship
  83. let newTempBasal = TempBasalStored(context: self.context)
  84. newTempBasal.pumpEvent = newPumpEvent
  85. newTempBasal.duration = Int16(round(minutes))
  86. newTempBasal.rate = rate as NSDecimalNumber
  87. newTempBasal.tempType = TempType.absolute.rawValue
  88. do {
  89. try CoreDataStack.shared.backgroundContext.saveContext()
  90. } catch {
  91. print(error.localizedDescription)
  92. }
  93. }
  94. return [
  95. PumpHistoryEvent(
  96. id: id,
  97. type: .tempBasalDuration,
  98. timestamp: date,
  99. amount: nil,
  100. duration: nil,
  101. durationMin: Int(round(minutes)),
  102. rate: nil,
  103. temp: nil,
  104. carbInput: nil
  105. ),
  106. PumpHistoryEvent(
  107. id: "_" + id,
  108. type: .tempBasal,
  109. timestamp: date,
  110. amount: nil,
  111. duration: nil,
  112. durationMin: nil,
  113. rate: rate,
  114. temp: .absolute,
  115. carbInput: nil
  116. )
  117. ]
  118. case .suspend:
  119. self.context.perform {
  120. // create pump event
  121. let newPumpEvent = PumpEventStored(context: self.context)
  122. newPumpEvent.id = id
  123. newPumpEvent.timestamp = event.date
  124. newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
  125. do {
  126. try CoreDataStack.shared.backgroundContext.saveContext()
  127. } catch {
  128. print(error.localizedDescription)
  129. }
  130. }
  131. return [
  132. PumpHistoryEvent(
  133. id: id,
  134. type: .pumpSuspend,
  135. timestamp: event.date,
  136. amount: nil,
  137. duration: nil,
  138. durationMin: nil,
  139. rate: nil,
  140. temp: nil,
  141. carbInput: nil
  142. )
  143. ]
  144. case .resume:
  145. self.context.perform {
  146. // create pump event
  147. let newPumpEvent = PumpEventStored(context: self.context)
  148. newPumpEvent.id = id
  149. newPumpEvent.timestamp = event.date
  150. newPumpEvent.type = PumpEvent.pumpResume.rawValue
  151. do {
  152. try CoreDataStack.shared.backgroundContext.saveContext()
  153. } catch {
  154. print(error.localizedDescription)
  155. }
  156. }
  157. return [
  158. PumpHistoryEvent(
  159. id: id,
  160. type: .pumpResume,
  161. timestamp: event.date,
  162. amount: nil,
  163. duration: nil,
  164. durationMin: nil,
  165. rate: nil,
  166. temp: nil,
  167. carbInput: nil
  168. )
  169. ]
  170. case .rewind:
  171. return [
  172. PumpHistoryEvent(
  173. id: id,
  174. type: .rewind,
  175. timestamp: event.date,
  176. amount: nil,
  177. duration: nil,
  178. durationMin: nil,
  179. rate: nil,
  180. temp: nil,
  181. carbInput: nil
  182. )
  183. ]
  184. case .prime:
  185. return [
  186. PumpHistoryEvent(
  187. id: id,
  188. type: .prime,
  189. timestamp: event.date,
  190. amount: nil,
  191. duration: nil,
  192. durationMin: nil,
  193. rate: nil,
  194. temp: nil,
  195. carbInput: nil
  196. )
  197. ]
  198. case .alarm:
  199. return [
  200. PumpHistoryEvent(
  201. id: id,
  202. type: .pumpAlarm,
  203. timestamp: event.date,
  204. note: event.title
  205. )
  206. ]
  207. default:
  208. return []
  209. }
  210. }
  211. self.storeEvents(eventsToStore)
  212. }
  213. }
  214. func storeJournalCarbs(_ carbs: Int) {
  215. processQueue.async {
  216. let eventsToStore = [
  217. PumpHistoryEvent(
  218. id: UUID().uuidString,
  219. type: .journalCarbs,
  220. timestamp: Date(),
  221. amount: nil,
  222. duration: nil,
  223. durationMin: nil,
  224. rate: nil,
  225. temp: nil,
  226. carbInput: carbs
  227. )
  228. ]
  229. self.storeEvents(eventsToStore)
  230. }
  231. }
  232. func storeEvents(_ events: [PumpHistoryEvent]) {
  233. processQueue.async {
  234. let file = OpenAPS.Monitor.pumpHistory
  235. var uniqEvents: [PumpHistoryEvent] = []
  236. self.storage.transaction { storage in
  237. storage.append(events, to: file, uniqBy: \.id)
  238. uniqEvents = storage.retrieve(file, as: [PumpHistoryEvent].self)?
  239. .filter { $0.timestamp.addingTimeInterval(1.days.timeInterval) > Date() }
  240. .sorted { $0.timestamp > $1.timestamp } ?? []
  241. storage.save(Array(uniqEvents), as: file)
  242. }
  243. self.broadcaster.notify(PumpHistoryObserver.self, on: self.processQueue) {
  244. $0.pumpHistoryDidUpdate(uniqEvents)
  245. }
  246. }
  247. }
  248. func recent() -> [PumpHistoryEvent] {
  249. storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
  250. }
  251. func deleteInsulin(at date: Date) {
  252. processQueue.sync {
  253. var allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
  254. guard let entryIndex = allValues.firstIndex(where: { $0.timestamp == date }) else {
  255. return
  256. }
  257. allValues.remove(at: entryIndex)
  258. storage.save(allValues, as: OpenAPS.Monitor.pumpHistory)
  259. broadcaster.notify(PumpHistoryObserver.self, on: processQueue) {
  260. $0.pumpHistoryDidUpdate(allValues)
  261. }
  262. }
  263. }
  264. func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
  265. if event.isSMB ?? false {
  266. return .smb
  267. }
  268. if event.isExternal ?? false {
  269. return .isExternal
  270. }
  271. return event.type
  272. }
  273. func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
  274. let events = recent()
  275. guard !events.isEmpty else { return [] }
  276. let temps: [NigtscoutTreatment] = events.reduce([]) { result, event in
  277. var result = result
  278. switch event.type {
  279. case .tempBasal:
  280. result.append(NigtscoutTreatment(
  281. duration: nil,
  282. rawDuration: nil,
  283. rawRate: event,
  284. absolute: event.rate,
  285. rate: event.rate,
  286. eventType: .nsTempBasal,
  287. createdAt: event.timestamp,
  288. enteredBy: NigtscoutTreatment.local,
  289. bolus: nil,
  290. insulin: nil,
  291. notes: nil,
  292. carbs: nil,
  293. fat: nil,
  294. protein: nil,
  295. targetTop: nil,
  296. targetBottom: nil
  297. ))
  298. case .tempBasalDuration:
  299. if var last = result.popLast(), last.eventType == .nsTempBasal, last.createdAt == event.timestamp {
  300. last.duration = event.durationMin
  301. last.rawDuration = event
  302. result.append(last)
  303. }
  304. default: break
  305. }
  306. return result
  307. }
  308. let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in
  309. switch event.type {
  310. case .bolus:
  311. let eventType = determineBolusEventType(for: event)
  312. return NigtscoutTreatment(
  313. duration: event.duration,
  314. rawDuration: nil,
  315. rawRate: nil,
  316. absolute: nil,
  317. rate: nil,
  318. eventType: eventType,
  319. createdAt: event.timestamp,
  320. enteredBy: NigtscoutTreatment.local,
  321. bolus: event,
  322. insulin: event.amount,
  323. notes: nil,
  324. carbs: nil,
  325. fat: nil,
  326. protein: nil,
  327. targetTop: nil,
  328. targetBottom: nil
  329. )
  330. case .journalCarbs:
  331. return NigtscoutTreatment(
  332. duration: nil,
  333. rawDuration: nil,
  334. rawRate: nil,
  335. absolute: nil,
  336. rate: nil,
  337. eventType: .nsCarbCorrection,
  338. createdAt: event.timestamp,
  339. enteredBy: NigtscoutTreatment.local,
  340. bolus: nil,
  341. insulin: nil,
  342. notes: nil,
  343. carbs: Decimal(event.carbInput ?? 0),
  344. fat: nil,
  345. protein: nil,
  346. targetTop: nil,
  347. targetBottom: nil
  348. )
  349. default: return nil
  350. }
  351. }
  352. let misc = events.compactMap { event -> NigtscoutTreatment? in
  353. switch event.type {
  354. case .prime:
  355. return NigtscoutTreatment(
  356. duration: event.duration,
  357. rawDuration: nil,
  358. rawRate: nil,
  359. absolute: nil,
  360. rate: nil,
  361. eventType: .nsSiteChange,
  362. createdAt: event.timestamp,
  363. enteredBy: NigtscoutTreatment.local,
  364. bolus: event,
  365. insulin: nil,
  366. notes: nil,
  367. carbs: nil,
  368. fat: nil,
  369. protein: nil,
  370. targetTop: nil,
  371. targetBottom: nil
  372. )
  373. case .rewind:
  374. return NigtscoutTreatment(
  375. duration: nil,
  376. rawDuration: nil,
  377. rawRate: nil,
  378. absolute: nil,
  379. rate: nil,
  380. eventType: .nsInsulinChange,
  381. createdAt: event.timestamp,
  382. enteredBy: NigtscoutTreatment.local,
  383. bolus: nil,
  384. insulin: nil,
  385. notes: nil,
  386. carbs: nil,
  387. fat: nil,
  388. protein: nil,
  389. targetTop: nil,
  390. targetBottom: nil
  391. )
  392. case .pumpAlarm:
  393. return NigtscoutTreatment(
  394. duration: 30, // minutes
  395. rawDuration: nil,
  396. rawRate: nil,
  397. absolute: nil,
  398. rate: nil,
  399. eventType: .nsAnnouncement,
  400. createdAt: event.timestamp,
  401. enteredBy: NigtscoutTreatment.local,
  402. bolus: nil,
  403. insulin: nil,
  404. notes: "Alarm \(String(describing: event.note)) \(event.type)",
  405. carbs: nil,
  406. fat: nil,
  407. protein: nil,
  408. targetTop: nil,
  409. targetBottom: nil
  410. )
  411. default: return nil
  412. }
  413. }
  414. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
  415. let treatments = Array(Set([bolusesAndCarbs, temps, misc].flatMap { $0 }).subtracting(Set(uploaded)))
  416. return treatments.sorted { $0.createdAt! > $1.createdAt! }
  417. }
  418. func saveCancelTempEvents() {
  419. let basalID = UUID().uuidString
  420. let date = Date()
  421. let events = [
  422. PumpHistoryEvent(
  423. id: basalID,
  424. type: .tempBasalDuration,
  425. timestamp: date,
  426. amount: nil,
  427. duration: nil,
  428. durationMin: 0,
  429. rate: nil,
  430. temp: nil,
  431. carbInput: nil
  432. ),
  433. PumpHistoryEvent(
  434. id: "_" + basalID,
  435. type: .tempBasal,
  436. timestamp: date,
  437. amount: nil,
  438. duration: nil,
  439. durationMin: nil,
  440. rate: 0,
  441. temp: .absolute,
  442. carbInput: nil
  443. )
  444. ]
  445. storeEvents(events)
  446. }
  447. }