PumpHistoryStorage.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. private let context = CoreDataStack.shared.viewContext
  25. func storePumpEvents(_ events: [NewPumpEvent]) {
  26. processQueue.async {
  27. let eventsToStore = events.flatMap { event -> [PumpHistoryEvent] in
  28. let id = event.raw.md5String
  29. switch event.type {
  30. case .bolus:
  31. guard let dose = event.dose else { return [] }
  32. let amount = Decimal(string: dose.unitsInDeliverableIncrements.description)
  33. let minutes = Int((dose.endDate - dose.startDate).timeInterval / 60)
  34. // MARK: - save to Core Data
  35. self.context.perform {
  36. let new = InsulinStored(context: self.context)
  37. new.amount = amount as? NSDecimalNumber
  38. new.date = Date()
  39. new.external = false
  40. new.id = UUID()
  41. new.isSMB = true
  42. }
  43. return [PumpHistoryEvent(
  44. id: id,
  45. type: .bolus,
  46. timestamp: event.date,
  47. amount: amount,
  48. duration: minutes,
  49. durationMin: nil,
  50. rate: nil,
  51. temp: nil,
  52. carbInput: nil,
  53. isSMB: dose.automatic,
  54. isExternal: dose.manuallyEntered
  55. )]
  56. case .tempBasal:
  57. guard let dose = event.dose else { return [] }
  58. let rate = Decimal(dose.unitsPerHour)
  59. let minutes = (dose.endDate - dose.startDate).timeInterval / 60
  60. let delivered = dose.deliveredUnits
  61. let date = event.date
  62. let isCancel = delivered != nil //! event.isMutable && delivered != nil
  63. guard !isCancel else { return [] }
  64. return [
  65. PumpHistoryEvent(
  66. id: id,
  67. type: .tempBasalDuration,
  68. timestamp: date,
  69. amount: nil,
  70. duration: nil,
  71. durationMin: Int(round(minutes)),
  72. rate: nil,
  73. temp: nil,
  74. carbInput: nil
  75. ),
  76. PumpHistoryEvent(
  77. id: "_" + id,
  78. type: .tempBasal,
  79. timestamp: date,
  80. amount: nil,
  81. duration: nil,
  82. durationMin: nil,
  83. rate: rate,
  84. temp: .absolute,
  85. carbInput: nil
  86. )
  87. ]
  88. case .suspend:
  89. return [
  90. PumpHistoryEvent(
  91. id: id,
  92. type: .pumpSuspend,
  93. timestamp: event.date,
  94. amount: nil,
  95. duration: nil,
  96. durationMin: nil,
  97. rate: nil,
  98. temp: nil,
  99. carbInput: nil
  100. )
  101. ]
  102. case .resume:
  103. return [
  104. PumpHistoryEvent(
  105. id: id,
  106. type: .pumpResume,
  107. timestamp: event.date,
  108. amount: nil,
  109. duration: nil,
  110. durationMin: nil,
  111. rate: nil,
  112. temp: nil,
  113. carbInput: nil
  114. )
  115. ]
  116. case .rewind:
  117. return [
  118. PumpHistoryEvent(
  119. id: id,
  120. type: .rewind,
  121. timestamp: event.date,
  122. amount: nil,
  123. duration: nil,
  124. durationMin: nil,
  125. rate: nil,
  126. temp: nil,
  127. carbInput: nil
  128. )
  129. ]
  130. case .prime:
  131. return [
  132. PumpHistoryEvent(
  133. id: id,
  134. type: .prime,
  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 .alarm:
  145. return [
  146. PumpHistoryEvent(
  147. id: id,
  148. type: .pumpAlarm,
  149. timestamp: event.date,
  150. note: event.title
  151. )
  152. ]
  153. default:
  154. return []
  155. }
  156. }
  157. self.storeEvents(eventsToStore)
  158. }
  159. }
  160. func storeJournalCarbs(_ carbs: Int) {
  161. processQueue.async {
  162. let eventsToStore = [
  163. PumpHistoryEvent(
  164. id: UUID().uuidString,
  165. type: .journalCarbs,
  166. timestamp: Date(),
  167. amount: nil,
  168. duration: nil,
  169. durationMin: nil,
  170. rate: nil,
  171. temp: nil,
  172. carbInput: carbs
  173. )
  174. ]
  175. self.storeEvents(eventsToStore)
  176. }
  177. }
  178. func storeEvents(_ events: [PumpHistoryEvent]) {
  179. processQueue.async {
  180. let file = OpenAPS.Monitor.pumpHistory
  181. var uniqEvents: [PumpHistoryEvent] = []
  182. self.storage.transaction { storage in
  183. storage.append(events, to: file, uniqBy: \.id)
  184. uniqEvents = storage.retrieve(file, as: [PumpHistoryEvent].self)?
  185. .filter { $0.timestamp.addingTimeInterval(1.days.timeInterval) > Date() }
  186. .sorted { $0.timestamp > $1.timestamp } ?? []
  187. storage.save(Array(uniqEvents), as: file)
  188. }
  189. self.broadcaster.notify(PumpHistoryObserver.self, on: self.processQueue) {
  190. $0.pumpHistoryDidUpdate(uniqEvents)
  191. }
  192. }
  193. }
  194. func recent() -> [PumpHistoryEvent] {
  195. storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
  196. }
  197. func deleteInsulin(at date: Date) {
  198. processQueue.sync {
  199. var allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
  200. guard let entryIndex = allValues.firstIndex(where: { $0.timestamp == date }) else {
  201. return
  202. }
  203. allValues.remove(at: entryIndex)
  204. storage.save(allValues, as: OpenAPS.Monitor.pumpHistory)
  205. broadcaster.notify(PumpHistoryObserver.self, on: processQueue) {
  206. $0.pumpHistoryDidUpdate(allValues)
  207. }
  208. }
  209. }
  210. func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
  211. if event.isSMB ?? false {
  212. return .smb
  213. }
  214. if event.isExternal ?? false {
  215. return .isExternal
  216. }
  217. return event.type
  218. }
  219. func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
  220. let events = recent()
  221. guard !events.isEmpty else { return [] }
  222. let temps: [NigtscoutTreatment] = events.reduce([]) { result, event in
  223. var result = result
  224. switch event.type {
  225. case .tempBasal:
  226. result.append(NigtscoutTreatment(
  227. duration: nil,
  228. rawDuration: nil,
  229. rawRate: event,
  230. absolute: event.rate,
  231. rate: event.rate,
  232. eventType: .nsTempBasal,
  233. createdAt: event.timestamp,
  234. enteredBy: NigtscoutTreatment.local,
  235. bolus: nil,
  236. insulin: nil,
  237. notes: nil,
  238. carbs: nil,
  239. fat: nil,
  240. protein: nil,
  241. targetTop: nil,
  242. targetBottom: nil
  243. ))
  244. case .tempBasalDuration:
  245. if var last = result.popLast(), last.eventType == .nsTempBasal, last.createdAt == event.timestamp {
  246. last.duration = event.durationMin
  247. last.rawDuration = event
  248. result.append(last)
  249. }
  250. default: break
  251. }
  252. return result
  253. }
  254. let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in
  255. switch event.type {
  256. case .bolus:
  257. let eventType = determineBolusEventType(for: event)
  258. return NigtscoutTreatment(
  259. duration: event.duration,
  260. rawDuration: nil,
  261. rawRate: nil,
  262. absolute: nil,
  263. rate: nil,
  264. eventType: eventType,
  265. createdAt: event.timestamp,
  266. enteredBy: NigtscoutTreatment.local,
  267. bolus: event,
  268. insulin: event.amount,
  269. notes: nil,
  270. carbs: nil,
  271. fat: nil,
  272. protein: nil,
  273. targetTop: nil,
  274. targetBottom: nil
  275. )
  276. case .journalCarbs:
  277. return NigtscoutTreatment(
  278. duration: nil,
  279. rawDuration: nil,
  280. rawRate: nil,
  281. absolute: nil,
  282. rate: nil,
  283. eventType: .nsCarbCorrection,
  284. createdAt: event.timestamp,
  285. enteredBy: NigtscoutTreatment.local,
  286. bolus: nil,
  287. insulin: nil,
  288. notes: nil,
  289. carbs: Decimal(event.carbInput ?? 0),
  290. fat: nil,
  291. protein: nil,
  292. targetTop: nil,
  293. targetBottom: nil
  294. )
  295. default: return nil
  296. }
  297. }
  298. let misc = events.compactMap { event -> NigtscoutTreatment? in
  299. switch event.type {
  300. case .prime:
  301. return NigtscoutTreatment(
  302. duration: event.duration,
  303. rawDuration: nil,
  304. rawRate: nil,
  305. absolute: nil,
  306. rate: nil,
  307. eventType: .nsSiteChange,
  308. createdAt: event.timestamp,
  309. enteredBy: NigtscoutTreatment.local,
  310. bolus: event,
  311. insulin: nil,
  312. notes: nil,
  313. carbs: nil,
  314. fat: nil,
  315. protein: nil,
  316. targetTop: nil,
  317. targetBottom: nil
  318. )
  319. case .rewind:
  320. return NigtscoutTreatment(
  321. duration: nil,
  322. rawDuration: nil,
  323. rawRate: nil,
  324. absolute: nil,
  325. rate: nil,
  326. eventType: .nsInsulinChange,
  327. createdAt: event.timestamp,
  328. enteredBy: NigtscoutTreatment.local,
  329. bolus: nil,
  330. insulin: nil,
  331. notes: nil,
  332. carbs: nil,
  333. fat: nil,
  334. protein: nil,
  335. targetTop: nil,
  336. targetBottom: nil
  337. )
  338. case .pumpAlarm:
  339. return NigtscoutTreatment(
  340. duration: 30, // minutes
  341. rawDuration: nil,
  342. rawRate: nil,
  343. absolute: nil,
  344. rate: nil,
  345. eventType: .nsAnnouncement,
  346. createdAt: event.timestamp,
  347. enteredBy: NigtscoutTreatment.local,
  348. bolus: nil,
  349. insulin: nil,
  350. notes: "Alarm \(String(describing: event.note)) \(event.type)",
  351. carbs: nil,
  352. fat: nil,
  353. protein: nil,
  354. targetTop: nil,
  355. targetBottom: nil
  356. )
  357. default: return nil
  358. }
  359. }
  360. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
  361. let treatments = Array(Set([bolusesAndCarbs, temps, misc].flatMap { $0 }).subtracting(Set(uploaded)))
  362. return treatments.sorted { $0.createdAt! > $1.createdAt! }
  363. }
  364. func saveCancelTempEvents() {
  365. let basalID = UUID().uuidString
  366. let date = Date()
  367. let events = [
  368. PumpHistoryEvent(
  369. id: basalID,
  370. type: .tempBasalDuration,
  371. timestamp: date,
  372. amount: nil,
  373. duration: nil,
  374. durationMin: 0,
  375. rate: nil,
  376. temp: nil,
  377. carbInput: nil
  378. ),
  379. PumpHistoryEvent(
  380. id: "_" + basalID,
  381. type: .tempBasal,
  382. timestamp: date,
  383. amount: nil,
  384. duration: nil,
  385. durationMin: nil,
  386. rate: 0,
  387. temp: .absolute,
  388. carbInput: nil
  389. )
  390. ]
  391. storeEvents(events)
  392. }
  393. }