PumpHistoryStorage.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. }
  15. final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
  16. private let processQueue = DispatchQueue(label: "BasePumpHistoryStorage.processQueue")
  17. @Injected() private var storage: FileStorage!
  18. @Injected() private var broadcaster: Broadcaster!
  19. init(resolver: Resolver) {
  20. injectServices(resolver)
  21. }
  22. func storePumpEvents(_ events: [NewPumpEvent]) {
  23. processQueue.async {
  24. let eventsToStore = events.flatMap { event -> [PumpHistoryEvent] in
  25. let id = event.raw.md5String
  26. switch event.type {
  27. case .bolus:
  28. guard let dose = event.dose else { return [] }
  29. let amount = Decimal(string: dose.unitsInDeliverableIncrements.description)
  30. let minutes = Int((dose.endDate - dose.startDate).timeInterval / 60)
  31. return [PumpHistoryEvent(
  32. id: id,
  33. type: .bolus,
  34. timestamp: event.date,
  35. amount: amount,
  36. duration: minutes,
  37. durationMin: nil,
  38. rate: nil,
  39. temp: nil,
  40. carbInput: nil
  41. )]
  42. case .tempBasal:
  43. guard let dose = event.dose else { return [] }
  44. let rate = Decimal(string: dose.unitsPerHour.description)
  45. let minutes = Int((dose.endDate - dose.startDate).timeInterval / 60)
  46. return [
  47. PumpHistoryEvent(
  48. id: id,
  49. type: .tempBasalDuration,
  50. timestamp: event.date,
  51. amount: nil,
  52. duration: nil,
  53. durationMin: minutes,
  54. rate: nil,
  55. temp: nil,
  56. carbInput: nil
  57. ),
  58. PumpHistoryEvent(
  59. id: "_" + id,
  60. type: .tempBasal,
  61. timestamp: event.date,
  62. amount: nil,
  63. duration: nil,
  64. durationMin: nil,
  65. rate: rate,
  66. temp: .absolute,
  67. carbInput: nil
  68. )
  69. ]
  70. case .suspend:
  71. return [
  72. PumpHistoryEvent(
  73. id: id,
  74. type: .pumpSuspend,
  75. timestamp: event.date,
  76. amount: nil,
  77. duration: nil,
  78. durationMin: nil,
  79. rate: nil,
  80. temp: nil,
  81. carbInput: nil
  82. )
  83. ]
  84. case .resume:
  85. return [
  86. PumpHistoryEvent(
  87. id: id,
  88. type: .pumpResume,
  89. timestamp: event.date,
  90. amount: nil,
  91. duration: nil,
  92. durationMin: nil,
  93. rate: nil,
  94. temp: nil,
  95. carbInput: nil
  96. )
  97. ]
  98. case .rewind:
  99. return [
  100. PumpHistoryEvent(
  101. id: id,
  102. type: .rewind,
  103. timestamp: event.date,
  104. amount: nil,
  105. duration: nil,
  106. durationMin: nil,
  107. rate: nil,
  108. temp: nil,
  109. carbInput: nil
  110. )
  111. ]
  112. case .prime:
  113. return [
  114. PumpHistoryEvent(
  115. id: id,
  116. type: .prime,
  117. timestamp: event.date,
  118. amount: nil,
  119. duration: nil,
  120. durationMin: nil,
  121. rate: nil,
  122. temp: nil,
  123. carbInput: nil
  124. )
  125. ]
  126. default:
  127. return []
  128. }
  129. }
  130. self.storeEvents(eventsToStore)
  131. }
  132. }
  133. func storeJournalCarbs(_ carbs: Int) {
  134. processQueue.async {
  135. let eventsToStore = [
  136. PumpHistoryEvent(
  137. id: UUID().uuidString,
  138. type: .journalCarbs,
  139. timestamp: Date(),
  140. amount: nil,
  141. duration: nil,
  142. durationMin: nil,
  143. rate: nil,
  144. temp: nil,
  145. carbInput: carbs
  146. )
  147. ]
  148. self.storeEvents(eventsToStore)
  149. }
  150. }
  151. func storeEvents(_ events: [PumpHistoryEvent]) {
  152. processQueue.async {
  153. let file = OpenAPS.Monitor.pumpHistory
  154. var uniqEvents: [PumpHistoryEvent] = []
  155. self.storage.transaction { storage in
  156. storage.append(events, to: file, uniqBy: \.id)
  157. uniqEvents = storage.retrieve(file, as: [PumpHistoryEvent].self)?
  158. .filter { $0.timestamp.addingTimeInterval(1.days.timeInterval) > Date() }
  159. .sorted { $0.timestamp > $1.timestamp } ?? []
  160. storage.save(Array(uniqEvents), as: file)
  161. }
  162. self.broadcaster.notify(PumpHistoryObserver.self, on: self.processQueue) {
  163. $0.pumpHistoryDidUpdate(uniqEvents)
  164. }
  165. }
  166. }
  167. func recent() -> [PumpHistoryEvent] {
  168. storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
  169. }
  170. func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
  171. let events = recent()
  172. guard !events.isEmpty else { return [] }
  173. let temps: [NigtscoutTreatment] = events.reduce([]) { result, event in
  174. var result = result
  175. switch event.type {
  176. case .tempBasal:
  177. result.append(NigtscoutTreatment(
  178. duration: nil,
  179. rawDuration: nil,
  180. rawRate: event,
  181. absolute: event.rate,
  182. rate: event.rate,
  183. eventType: .nsTempBasal,
  184. createdAt: event.timestamp,
  185. enteredBy: NigtscoutTreatment.local,
  186. bolus: nil,
  187. insulin: nil,
  188. notes: nil,
  189. carbs: nil,
  190. targetTop: nil,
  191. targetBottom: nil
  192. ))
  193. case .tempBasalDuration:
  194. if var last = result.popLast(), last.eventType == .nsTempBasal, last.createdAt == event.timestamp {
  195. last.duration = event.durationMin
  196. last.rawDuration = event
  197. result.append(last)
  198. }
  199. default: break
  200. }
  201. return result
  202. }
  203. let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in
  204. switch event.type {
  205. case .bolus:
  206. return NigtscoutTreatment(
  207. duration: event.duration,
  208. rawDuration: nil,
  209. rawRate: nil,
  210. absolute: nil,
  211. rate: nil,
  212. eventType: .bolus,
  213. createdAt: event.timestamp,
  214. enteredBy: NigtscoutTreatment.local,
  215. bolus: event,
  216. insulin: event.amount,
  217. notes: nil,
  218. carbs: nil,
  219. targetTop: nil,
  220. targetBottom: nil
  221. )
  222. case .journalCarbs:
  223. return NigtscoutTreatment(
  224. duration: nil,
  225. rawDuration: nil,
  226. rawRate: nil,
  227. absolute: nil,
  228. rate: nil,
  229. eventType: .nsCarbCorrection,
  230. createdAt: event.timestamp,
  231. enteredBy: NigtscoutTreatment.local,
  232. bolus: nil,
  233. insulin: nil,
  234. notes: nil,
  235. carbs: Decimal(event.carbInput ?? 0),
  236. targetTop: nil,
  237. targetBottom: nil
  238. )
  239. default: return nil
  240. }
  241. }
  242. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
  243. let treatments = Array(Set([bolusesAndCarbs, temps].flatMap { $0 }).subtracting(Set(uploaded)))
  244. return treatments.sorted { $0.createdAt! > $1.createdAt! }
  245. }
  246. }