PumpEvent+helper.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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 duplicateInLastHour(_ date: Date) -> NSPredicate {
  98. let date60m = Date.oneHourAgo
  99. return NSPredicate(format: "timestamp >= %@ && timestamp == %@", date60m as NSDate, date as NSDate)
  100. }
  101. static var pumpEventsNotYetUploadedToNightscout: NSPredicate {
  102. let date = Date.oneDayAgo
  103. return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
  104. }
  105. static var pumpEventsNotYetUploadedToHealth: NSPredicate {
  106. let date = Date.oneDayAgo
  107. return NSPredicate(format: "timestamp >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
  108. }
  109. static var pumpEventsNotYetUploadedToTidepool: NSPredicate {
  110. let date = Date.oneDayAgo
  111. return NSPredicate(format: "timestamp >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
  112. }
  113. }
  114. // MARK: - PumpEventDTO and Conformance to ImportableDTO
  115. enum PumpEventDTO: Encodable, Decodable, ImportableDTO {
  116. case bolus(BolusDTO)
  117. case tempBasal(TempBasalDTO)
  118. case tempBasalDuration(TempBasalDurationDTO)
  119. case suspend(SuspendDTO)
  120. case resume(ResumeDTO)
  121. case rewind(RewindDTO)
  122. case prime(PrimeDTO)
  123. case unknown(String) // Catch-all for unknown types
  124. func encode(to encoder: Encoder) throws {
  125. switch self {
  126. case let .bolus(bolus):
  127. try bolus.encode(to: encoder)
  128. case let .tempBasal(tempBasal):
  129. try tempBasal.encode(to: encoder)
  130. case let .tempBasalDuration(tempBasalDuration):
  131. try tempBasalDuration.encode(to: encoder)
  132. case let .suspend(suspend):
  133. try suspend.encode(to: encoder)
  134. case let .resume(resume):
  135. try resume.encode(to: encoder)
  136. case let .rewind(rewind):
  137. try rewind.encode(to: encoder)
  138. case let .prime(prime):
  139. try prime.encode(to: encoder)
  140. case let .unknown(type):
  141. debugPrint("⚠️ Skipping unknown type during encoding: \(type)")
  142. }
  143. }
  144. private enum CodingKeys: String, CodingKey {
  145. case _type
  146. }
  147. init(from decoder: Decoder) throws {
  148. let container = try decoder.container(keyedBy: CodingKeys.self)
  149. // Attempt to decode `_type` key
  150. guard let type = try? container.decode(String.self, forKey: ._type) else {
  151. debugPrint("⚠️ Missing _type in JSON entry.")
  152. self = .unknown("missing_type")
  153. return
  154. }
  155. let singleValueContainer = try decoder.singleValueContainer()
  156. switch type {
  157. case "Bolus":
  158. let bolusDTO = try singleValueContainer.decode(BolusDTO.self)
  159. self = .bolus(bolusDTO)
  160. case "TempBasal":
  161. let tempBasalDTO = try singleValueContainer.decode(TempBasalDTO.self)
  162. self = .tempBasal(tempBasalDTO)
  163. case "TempBasalDuration":
  164. let tempBasalDurationDTO = try singleValueContainer.decode(TempBasalDurationDTO.self)
  165. self = .tempBasalDuration(tempBasalDurationDTO)
  166. case "PumpSuspend":
  167. let SuspendDTO = try singleValueContainer.decode(SuspendDTO.self)
  168. self = .suspend(SuspendDTO)
  169. case "PumpResume":
  170. let ResumeDTO = try singleValueContainer.decode(ResumeDTO.self)
  171. self = .resume(ResumeDTO)
  172. default:
  173. debugPrint("⚠️ Unknown _type value: \(type)")
  174. self = .unknown(type)
  175. }
  176. }
  177. // Conformance to ImportableDTO
  178. typealias ManagedObject = PumpEventStored
  179. func store(in context: NSManagedObjectContext) -> PumpEventStored {
  180. let pumpEvent = PumpEventStored(context: context)
  181. pumpEvent.isUploadedToNS = true
  182. pumpEvent.isUploadedToHealth = true
  183. pumpEvent.isUploadedToTidepool = true
  184. switch self {
  185. case let .bolus(bolusDTO):
  186. pumpEvent.id = bolusDTO.id
  187. pumpEvent.timestamp = ISO8601DateFormatter().date(from: bolusDTO.timestamp)
  188. pumpEvent.type = bolusDTO._type
  189. let bolus = BolusStored(context: context)
  190. bolus.amount = NSDecimalNumber(value: bolusDTO.amount)
  191. bolus.isExternal = bolusDTO.isExternal
  192. bolus.isSMB = bolusDTO.isSMB
  193. pumpEvent.bolus = bolus
  194. return pumpEvent
  195. case let .tempBasal(tempBasalDTO):
  196. pumpEvent.id = tempBasalDTO.id
  197. pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDTO.timestamp)
  198. pumpEvent.type = tempBasalDTO._type
  199. let tempBasal = TempBasalStored(context: context)
  200. tempBasal.tempType = tempBasalDTO.temp
  201. tempBasal.rate = NSDecimalNumber(value: tempBasalDTO.rate)
  202. pumpEvent.tempBasal = tempBasal
  203. return pumpEvent
  204. case let .tempBasalDuration(tempBasalDurationDTO):
  205. pumpEvent.id = tempBasalDurationDTO.id
  206. pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDurationDTO.timestamp)
  207. pumpEvent.type = tempBasalDurationDTO._type
  208. let tempBasal = TempBasalStored(context: context)
  209. tempBasal.duration = Int16(tempBasalDurationDTO.duration)
  210. pumpEvent.tempBasal = tempBasal
  211. return pumpEvent
  212. case let .suspend(SuspendDTO):
  213. pumpEvent.id = SuspendDTO.id
  214. pumpEvent.timestamp = ISO8601DateFormatter().date(from: SuspendDTO.timestamp)
  215. pumpEvent.type = SuspendDTO._type
  216. return pumpEvent
  217. case let .resume(ResumeDTO):
  218. pumpEvent.id = ResumeDTO.id
  219. pumpEvent.timestamp = ISO8601DateFormatter().date(from: ResumeDTO.timestamp)
  220. pumpEvent.type = ResumeDTO._type
  221. return pumpEvent
  222. case let .rewind(RewindDTO):
  223. pumpEvent.id = RewindDTO.id
  224. pumpEvent.timestamp = ISO8601DateFormatter().date(from: RewindDTO.timestamp)
  225. pumpEvent.type = RewindDTO._type
  226. return pumpEvent
  227. case let .prime(PrimeDTO):
  228. pumpEvent.id = PrimeDTO.id
  229. pumpEvent.timestamp = ISO8601DateFormatter().date(from: PrimeDTO.timestamp)
  230. pumpEvent.type = PrimeDTO._type
  231. return pumpEvent
  232. case .unknown:
  233. debugPrint("⚠️ Skipping unknown event type.")
  234. // Return an empty PumpEventStored object or handle appropriately
  235. return PumpEventStored(context: context)
  236. }
  237. }
  238. }
  239. // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
  240. struct BolusDTO: Codable {
  241. var id: String
  242. var timestamp: String
  243. var amount: Double
  244. var isExternal: Bool
  245. var isSMB: Bool = false
  246. var duration: Int
  247. var _type: String = EventType.bolus.rawValue
  248. private enum CodingKeys: String, CodingKey {
  249. case id
  250. case timestamp
  251. case amount
  252. case isExternal = "isExternalInsulin"
  253. case isSMB
  254. case duration
  255. case _type
  256. }
  257. }
  258. struct TempBasalDTO: Codable {
  259. var id: String
  260. var timestamp: String
  261. var temp: String
  262. var rate: Double
  263. var _type: String = EventType.tempBasal.rawValue
  264. }
  265. struct TempBasalDurationDTO: Codable {
  266. var id: String
  267. var timestamp: String
  268. var duration: Int
  269. var _type: String = EventType.tempBasalDuration.rawValue
  270. private enum CodingKeys: String, CodingKey {
  271. case id
  272. case timestamp
  273. case duration = "duration (min)"
  274. case _type
  275. }
  276. }
  277. struct SuspendDTO: Codable {
  278. var id: String
  279. var timestamp: String
  280. var _type: String = EventType.pumpSuspend.rawValue
  281. }
  282. struct ResumeDTO: Codable {
  283. var id: String
  284. var timestamp: String
  285. var _type: String = EventType.pumpResume.rawValue
  286. }
  287. struct RewindDTO: Codable {
  288. var id: String
  289. var timestamp: String
  290. var _type: String = EventType.rewind.rawValue
  291. }
  292. struct PrimeDTO: Codable {
  293. var id: String
  294. var timestamp: String
  295. var _type: String = EventType.prime.rawValue
  296. }
  297. // Extension with helper functions to map pump events to DTO objects via uniform masking enum
  298. extension PumpEventStored {
  299. static let dateFormatter: ISO8601DateFormatter = {
  300. let formatter = ISO8601DateFormatter()
  301. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  302. return formatter
  303. }()
  304. func toBolusDTOEnum() -> PumpEventDTO? {
  305. guard let timestamp = timestamp, let bolus = bolus, let amount = bolus.amount else {
  306. return nil
  307. }
  308. let bolusDTO = BolusDTO(
  309. id: id ?? UUID().uuidString,
  310. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  311. amount: amount.doubleValue,
  312. isExternal: bolus.isExternal,
  313. isSMB: bolus.isSMB,
  314. duration: 0
  315. )
  316. return .bolus(bolusDTO)
  317. }
  318. func toTempBasalDTOEnum() -> PumpEventDTO? {
  319. guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
  320. return nil
  321. }
  322. let tempBasalDTO = TempBasalDTO(
  323. id: "_\(id)",
  324. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  325. temp: tempBasal.tempType ?? "unknown",
  326. rate: rate.doubleValue
  327. )
  328. return .tempBasal(tempBasalDTO)
  329. }
  330. func toTempBasalDurationDTOEnum() -> PumpEventDTO? {
  331. guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal else {
  332. return nil
  333. }
  334. let tempBasalDurationDTO = TempBasalDurationDTO(
  335. id: id,
  336. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  337. duration: Int(tempBasal.duration)
  338. )
  339. return .tempBasalDuration(tempBasalDurationDTO)
  340. }
  341. func toSuspendDTO() -> PumpEventDTO? {
  342. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpSuspend.rawValue else {
  343. return nil
  344. }
  345. let suspendDTO = SuspendDTO(
  346. id: id,
  347. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  348. )
  349. return .suspend(suspendDTO)
  350. }
  351. func toResumeDTO() -> PumpEventDTO? {
  352. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpResume.rawValue else {
  353. return nil
  354. }
  355. let resumeDTO = ResumeDTO(
  356. id: id,
  357. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  358. )
  359. return .resume(resumeDTO)
  360. }
  361. func toRewindDTO() -> PumpEventDTO? {
  362. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.rewind.rawValue else {
  363. return nil
  364. }
  365. let rewindDTO = RewindDTO(
  366. id: id,
  367. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  368. )
  369. return .rewind(rewindDTO)
  370. }
  371. func toPrimeDTO() -> PumpEventDTO? {
  372. guard let id = id, let timestamp = timestamp, let type = type, type == EventType.prime.rawValue else {
  373. return nil
  374. }
  375. let primeDTO = PrimeDTO(
  376. id: id,
  377. timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
  378. )
  379. return .prime(primeDTO)
  380. }
  381. }