PumpHistoryStorage.swift 9.7 KB

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