PumpEvent+helper.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import CoreData
  2. import Foundation
  3. extension PumpEventStored {
  4. static func fetch(_ predicate: NSPredicate, ascending: Bool, fetchLimit: Int? = nil) -> NSFetchRequest<PumpEventStored> {
  5. let request = PumpEventStored.fetchRequest()
  6. request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: ascending)]
  7. request.resultType = .managedObjectResultType
  8. request.predicate = predicate
  9. if let fetchLimit = fetchLimit {
  10. request.fetchLimit = fetchLimit
  11. }
  12. return request
  13. }
  14. // Preview
  15. @discardableResult static func makePreviewEvents(count: Int, provider: CoreDataStack) -> [PumpEventStored] {
  16. let context = provider.persistentContainer.viewContext
  17. let events = (0 ..< count).map { index -> PumpEventStored in
  18. let event = PumpEventStored(context: context)
  19. event.id = UUID().uuidString
  20. event.timestamp = Date.now.addingTimeInterval(Double(index) * -300) // Every 5 minutes
  21. event.type = EventType.bolus.rawValue
  22. event.isUploadedToNS = false
  23. event.isUploadedToHealth = false
  24. event.isUploadedToTidepool = false
  25. // Add a bolus
  26. let bolus = BolusStored(context: context)
  27. bolus.amount = 2.5 as NSDecimalNumber
  28. bolus.isExternal = false
  29. bolus.isSMB = false
  30. bolus.pumpEvent = event
  31. return event
  32. }
  33. try? context.save()
  34. return events
  35. }
  36. }
  37. public extension PumpEventStored {
  38. enum EventType: String, JSON {
  39. case bolus = "Bolus"
  40. case smb = "SMB"
  41. case isExternal = "External Insulin"
  42. case mealBolus = "Meal Bolus"
  43. case correctionBolus = "Correction Bolus"
  44. case snackBolus = "Snack Bolus"
  45. case bolusWizard = "BolusWizard"
  46. case tempBasal = "TempBasal"
  47. case tempBasalDuration = "TempBasalDuration"
  48. case pumpSuspend = "PumpSuspend"
  49. case pumpResume = "PumpResume"
  50. case pumpAlarm = "PumpAlarm"
  51. case pumpBattery = "PumpBattery"
  52. case rewind = "Rewind"
  53. case prime = "Prime"
  54. case journalCarbs = "JournalEntryMealMarker"
  55. case nsNote = "Note"
  56. case nsTempBasal = "Temp Basal"
  57. case nsCarbCorrection = "Carb Correction"
  58. case nsTempTarget = "Temporary Target"
  59. case nsInsulinChange = "Insulin Change"
  60. case nsSiteChange = "Site Change"
  61. case nsBatteryChange = "Pump Battery Change"
  62. case nsAnnouncement = "Announcement"
  63. case nsSensorChange = "Sensor Start"
  64. case nsExercise = "Exercise"
  65. case capillaryGlucose = "BG Check"
  66. }
  67. enum TempType: String, JSON {
  68. case absolute
  69. case percent
  70. }
  71. }
  72. extension NSPredicate {
  73. static var pumpHistoryLast1440Minutes: NSPredicate {
  74. let date = Date.oneDayAgoInMinutes
  75. return NSPredicate(format: "timestamp >= %@", date as NSDate)
  76. }
  77. static var pumpHistoryLast24h: NSPredicate {
  78. let date = Date.oneDayAgo
  79. return NSPredicate(format: "timestamp >= %@", date as NSDate)
  80. }
  81. static var pumpHistoryForStats: NSPredicate {
  82. let date = Date.threeMonthsAgo
  83. return NSPredicate(format: "pumpEvent.timestamp >= %@", date as NSDate)
  84. }
  85. static var recentPumpHistory: NSPredicate {
  86. let date = Date.twentyMinutesAgo
  87. return NSPredicate(
  88. format: "type == %@ AND timestamp >= %@",
  89. PumpEventStored.EventType.tempBasal.rawValue,
  90. date as NSDate
  91. )
  92. }
  93. static var lastPumpBolus: NSPredicate {
  94. let date = Date.twentyMinutesAgo
  95. return NSPredicate(format: "timestamp >= %@ AND bolus.isExternal == %@", date as NSDate, false as NSNumber)
  96. }
  97. static func duplicates(_ date: Date) -> NSPredicate {
  98. NSPredicate(format: "timestamp == %@", date as NSDate)
  99. }
  100. static var pumpEventsNotYetUploadedToNightscout: NSPredicate {
  101. let date = Date.oneDayAgo
  102. return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
  103. }
  104. static var pumpEventsNotYetUploadedToHealth: NSPredicate {
  105. let date = Date.oneDayAgo
  106. return NSPredicate(format: "timestamp >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
  107. }
  108. static var pumpEventsNotYetUploadedToTidepool: NSPredicate {
  109. let date = Date.oneDayAgo
  110. return NSPredicate(format: "timestamp >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
  111. }
  112. }
  113. // MARK: - PumpEventDTO and Conformance to ImportableDTO
  114. enum PumpEventDTO: Encodable, Decodable, ImportableDTO {
  115. case bolus(BolusDTO)
  116. case tempBasal(TempBasalDTO)
  117. case tempBasalDuration(TempBasalDurationDTO)
  118. case suspend(SuspendDTO)
  119. case resume(ResumeDTO)
  120. case rewind(RewindDTO)
  121. case prime(PrimeDTO)
  122. case unknown(String) // Catch-all for unknown types
  123. func encode(to encoder: Encoder) throws {
  124. switch self {
  125. case let .bolus(bolus):
  126. try bolus.encode(to: encoder)
  127. case let .tempBasal(tempBasal):
  128. try tempBasal.encode(to: encoder)
  129. case let .tempBasalDuration(tempBasalDuration):
  130. try tempBasalDuration.encode(to: encoder)
  131. case let .suspend(suspend):
  132. try suspend.encode(to: encoder)
  133. case let .resume(resume):
  134. try resume.encode(to: encoder)
  135. case let .rewind(rewind):
  136. try rewind.encode(to: encoder)
  137. case let .prime(prime):
  138. try prime.encode(to: encoder)
  139. case let .unknown(type):
  140. debugPrint("⚠️ Skipping unknown type during encoding: \(type)")
  141. }
  142. }
  143. private enum CodingKeys: String, CodingKey {
  144. case _type
  145. }
  146. init(from decoder: Decoder) throws {
  147. let container = try decoder.container(keyedBy: CodingKeys.self)
  148. // Attempt to decode `_type` key
  149. guard let type = try? container.decode(String.self, forKey: ._type) else {
  150. debugPrint("⚠️ Missing _type in JSON entry.")
  151. self = .unknown("missing_type")
  152. return
  153. }
  154. let singleValueContainer = try decoder.singleValueContainer()
  155. switch type {
  156. case "Bolus":
  157. let bolusDTO = try singleValueContainer.decode(BolusDTO.self)
  158. self = .bolus(bolusDTO)
  159. case "TempBasal":
  160. let tempBasalDTO = try singleValueContainer.decode(TempBasalDTO.self)
  161. self = .tempBasal(tempBasalDTO)
  162. case "TempBasalDuration":
  163. let tempBasalDurationDTO = try singleValueContainer.decode(TempBasalDurationDTO.self)
  164. self = .tempBasalDuration(tempBasalDurationDTO)
  165. case "PumpSuspend":
  166. let SuspendDTO = try singleValueContainer.decode(SuspendDTO.self)
  167. self = .suspend(SuspendDTO)
  168. case "PumpResume":
  169. let ResumeDTO = try singleValueContainer.decode(ResumeDTO.self)
  170. self = .resume(ResumeDTO)
  171. default:
  172. debugPrint("⚠️ Unknown _type value: \(type)")
  173. self = .unknown(type)
  174. }
  175. }
  176. // Conformance to ImportableDTO
  177. typealias ManagedObject = PumpEventStored
  178. func store(in context: NSManagedObjectContext) -> PumpEventStored {
  179. let pumpEvent = PumpEventStored(context: context)
  180. pumpEvent.isUploadedToNS = true
  181. pumpEvent.isUploadedToHealth = true
  182. pumpEvent.isUploadedToTidepool = true
  183. switch self {
  184. case let .bolus(bolusDTO):
  185. pumpEvent.id = bolusDTO.id
  186. pumpEvent.timestamp = ISO8601DateFormatter().date(from: bolusDTO.timestamp)
  187. pumpEvent.type = bolusDTO._type
  188. let bolus = BolusStored(context: context)
  189. bolus.amount = NSDecimalNumber(value: bolusDTO.amount)
  190. bolus.isExternal = bolusDTO.isExternal
  191. bolus.isSMB = bolusDTO.isSMB
  192. pumpEvent.bolus = bolus
  193. return pumpEvent
  194. case let .tempBasal(tempBasalDTO):
  195. pumpEvent.id = tempBasalDTO.id
  196. pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDTO.timestamp)
  197. pumpEvent.type = tempBasalDTO._type
  198. let tempBasal = TempBasalStored(context: context)
  199. tempBasal.tempType = tempBasalDTO.temp
  200. tempBasal.rate = NSDecimalNumber(value: tempBasalDTO.rate)
  201. pumpEvent.tempBasal = tempBasal
  202. return pumpEvent
  203. case let .tempBasalDuration(tempBasalDurationDTO):
  204. pumpEvent.id = tempBasalDurationDTO.id
  205. pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDurationDTO.timestamp)
  206. pumpEvent.type = tempBasalDurationDTO._type
  207. let tempBasal = TempBasalStored(context: context)
  208. tempBasal.duration = Int16(tempBasalDurationDTO.duration)
  209. pumpEvent.tempBasal = tempBasal
  210. return pumpEvent
  211. case let .suspend(SuspendDTO):
  212. pumpEvent.id = SuspendDTO.id
  213. pumpEvent.timestamp = ISO8601DateFormatter().date(from: SuspendDTO.timestamp)
  214. pumpEvent.type = SuspendDTO._type
  215. return pumpEvent
  216. case let .resume(ResumeDTO):
  217. pumpEvent.id = ResumeDTO.id
  218. pumpEvent.timestamp = ISO8601DateFormatter().date(from: ResumeDTO.timestamp)
  219. pumpEvent.type = ResumeDTO._type
  220. return pumpEvent
  221. case let .rewind(RewindDTO):
  222. pumpEvent.id = RewindDTO.id
  223. pumpEvent.timestamp = ISO8601DateFormatter().date(from: RewindDTO.timestamp)
  224. pumpEvent.type = RewindDTO._type
  225. return pumpEvent
  226. case let .prime(PrimeDTO):
  227. pumpEvent.id = PrimeDTO.id
  228. pumpEvent.timestamp = ISO8601DateFormatter().date(from: PrimeDTO.timestamp)
  229. pumpEvent.type = PrimeDTO._type
  230. return pumpEvent
  231. case .unknown:
  232. debugPrint("⚠️ Skipping unknown event type.")
  233. // Return an empty PumpEventStored object or handle appropriately
  234. return PumpEventStored(context: context)
  235. }
  236. }
  237. }
  238. // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
  239. struct BolusDTO: Codable {
  240. var id: String
  241. var timestamp: String
  242. var amount: Double
  243. var isExternal: Bool
  244. var isSMB: Bool = false
  245. var duration: Int
  246. var _type: String = EventType.bolus.rawValue
  247. private enum CodingKeys: String, CodingKey {
  248. case id
  249. case timestamp
  250. case amount
  251. case isExternal = "isExternalInsulin"
  252. case isSMB
  253. case duration
  254. case _type
  255. }
  256. }
  257. struct TempBasalDTO: Codable {
  258. var id: String
  259. var timestamp: String
  260. var temp: String
  261. var rate: Double
  262. var _type: String = EventType.tempBasal.rawValue
  263. }
  264. struct TempBasalDurationDTO: Codable {
  265. var id: String
  266. var timestamp: String
  267. var duration: Int
  268. var _type: String = EventType.tempBasalDuration.rawValue
  269. private enum CodingKeys: String, CodingKey {
  270. case id
  271. case timestamp
  272. case duration = "duration (min)"
  273. case _type
  274. }
  275. }
  276. struct SuspendDTO: Codable {
  277. var id: String
  278. var timestamp: String
  279. var _type: String = EventType.pumpSuspend.rawValue
  280. }
  281. struct ResumeDTO: Codable {
  282. var id: String
  283. var timestamp: String
  284. var _type: String = EventType.pumpResume.rawValue
  285. }
  286. struct RewindDTO: Codable {
  287. var id: String
  288. var timestamp: String
  289. var _type: String = EventType.rewind.rawValue
  290. }
  291. struct PrimeDTO: Codable {
  292. var id: String
  293. var timestamp: String
  294. var _type: String = EventType.prime.rawValue
  295. }
  296. // Extension with helper functions to map pump events to DTO objects via uniform masking enum
  297. extension PumpEventStored {
  298. static let dateFormatter: ISO8601DateFormatter = {
  299. let formatter = ISO8601DateFormatter()
  300. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  301. return formatter
  302. }()
  303. func toBolusDTOEnum() -> PumpEventDTO? {
  304. guard let timestamp = timestamp, let bolus = bolus, let amount = bolus.amount else {
  305. return nil
  306. }
  307. let bolusDTO = BolusDTO(
  308. id: id ?? UUID().uuidString,
  309. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  310. amount: amount.doubleValue,
  311. isExternal: bolus.isExternal,
  312. isSMB: bolus.isSMB,
  313. duration: 0
  314. )
  315. return .bolus(bolusDTO)
  316. }
  317. func toTempBasalDTOEnum() -> PumpEventDTO? {
  318. guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
  319. return nil
  320. }
  321. let tempBasalDTO = TempBasalDTO(
  322. id: "_\(id)",
  323. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  324. temp: tempBasal.tempType ?? "unknown",
  325. rate: rate.doubleValue
  326. )
  327. return .tempBasal(tempBasalDTO)
  328. }
  329. func toTempBasalDurationDTOEnum() -> PumpEventDTO? {
  330. guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal else {
  331. return nil
  332. }
  333. let tempBasalDurationDTO = TempBasalDurationDTO(
  334. id: id,
  335. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  336. duration: Int(tempBasal.duration)
  337. )
  338. return .tempBasalDuration(tempBasalDurationDTO)
  339. }
  340. func toSuspendDTO() -> PumpEventDTO? {
  341. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpSuspend.rawValue else {
  342. return nil
  343. }
  344. let suspendDTO = SuspendDTO(
  345. id: id,
  346. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  347. )
  348. return .suspend(suspendDTO)
  349. }
  350. func toResumeDTO() -> PumpEventDTO? {
  351. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpResume.rawValue else {
  352. return nil
  353. }
  354. let resumeDTO = ResumeDTO(
  355. id: id,
  356. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  357. )
  358. return .resume(resumeDTO)
  359. }
  360. func toRewindDTO() -> PumpEventDTO? {
  361. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.rewind.rawValue else {
  362. return nil
  363. }
  364. let rewindDTO = RewindDTO(
  365. id: id,
  366. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  367. )
  368. return .rewind(rewindDTO)
  369. }
  370. func toPrimeDTO() -> PumpEventDTO? {
  371. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.prime.rawValue else {
  372. return nil
  373. }
  374. let primeDTO = PrimeDTO(
  375. id: id,
  376. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  377. )
  378. return .prime(primeDTO)
  379. }
  380. }