PumpEvent+helper.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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. }
  15. public extension PumpEventStored {
  16. enum EventType: String, JSON {
  17. case bolus = "Bolus"
  18. case smb = "SMB"
  19. case isExternal = "External Insulin"
  20. case mealBolus = "Meal Bolus"
  21. case correctionBolus = "Correction Bolus"
  22. case snackBolus = "Snack Bolus"
  23. case bolusWizard = "BolusWizard"
  24. case tempBasal = "TempBasal"
  25. case tempBasalDuration = "TempBasalDuration"
  26. case pumpSuspend = "PumpSuspend"
  27. case pumpResume = "PumpResume"
  28. case pumpAlarm = "PumpAlarm"
  29. case pumpBattery = "PumpBattery"
  30. case rewind = "Rewind"
  31. case prime = "Prime"
  32. case journalCarbs = "JournalEntryMealMarker"
  33. case nsNote = "Note"
  34. case nsTempBasal = "Temp Basal"
  35. case nsCarbCorrection = "Carb Correction"
  36. case nsTempTarget = "Temporary Target"
  37. case nsInsulinChange = "Insulin Change"
  38. case nsSiteChange = "Site Change"
  39. case nsBatteryChange = "Pump Battery Change"
  40. case nsAnnouncement = "Announcement"
  41. case nsSensorChange = "Sensor Start"
  42. case nsExercise = "Exercise"
  43. case capillaryGlucose = "BG Check"
  44. }
  45. enum TempType: String, JSON {
  46. case absolute
  47. case percent
  48. }
  49. }
  50. extension NSPredicate {
  51. static var pumpHistoryLast1440Minutes: NSPredicate {
  52. let date = Date.oneDayAgoInMinutes
  53. return NSPredicate(format: "timestamp >= %@", date as NSDate)
  54. }
  55. static var pumpHistoryLast24h: NSPredicate {
  56. let date = Date.oneDayAgo
  57. return NSPredicate(format: "timestamp >= %@", date as NSDate)
  58. }
  59. static var recentPumpHistory: NSPredicate {
  60. let date = Date.twentyMinutesAgo
  61. return NSPredicate(format: "timestamp >= %@", date as NSDate)
  62. }
  63. static var lastPumpBolus: NSPredicate {
  64. let date = Date.twentyMinutesAgo
  65. return NSPredicate(format: "timestamp >= %@ AND bolus.isExternal == %@", date as NSDate, false as NSNumber)
  66. }
  67. static func duplicateInLastHour(_ date: Date) -> NSPredicate {
  68. let date60m = Date.oneHourAgo
  69. return NSPredicate(format: "timestamp >= %@ && timestamp == %@", date60m as NSDate, date as NSDate)
  70. }
  71. static var pumpEventsNotYetUploadedToNightscout: NSPredicate {
  72. let date = Date.oneDayAgo
  73. return NSPredicate(format: "timestamp >= %@ AND isUploadedToNS == %@", date as NSDate, false as NSNumber)
  74. }
  75. static var pumpEventsNotYetUploadedToHealth: NSPredicate {
  76. let date = Date.oneDayAgo
  77. return NSPredicate(format: "timestamp >= %@ AND isUploadedToHealth == %@", date as NSDate, false as NSNumber)
  78. }
  79. static var pumpEventsNotYetUploadedToTidepool: NSPredicate {
  80. let date = Date.oneDayAgo
  81. return NSPredicate(format: "timestamp >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
  82. }
  83. }
  84. // MARK: - PumpEventDTO and Conformance to ImportableDTO
  85. enum PumpEventDTO: Encodable, Decodable, ImportableDTO {
  86. case bolus(BolusDTO)
  87. case tempBasal(TempBasalDTO)
  88. case tempBasalDuration(TempBasalDurationDTO)
  89. case pumpSuspend(PumpSuspendDTO)
  90. case unknown(String) // Catch-all for unknown types
  91. func encode(to encoder: Encoder) throws {
  92. switch self {
  93. case let .bolus(bolus):
  94. try bolus.encode(to: encoder)
  95. case let .tempBasal(tempBasal):
  96. try tempBasal.encode(to: encoder)
  97. case let .tempBasalDuration(tempBasalDuration):
  98. try tempBasalDuration.encode(to: encoder)
  99. case let .pumpSuspend(pumpSuspend):
  100. try pumpSuspend.encode(to: encoder)
  101. case let .unknown(type):
  102. debugPrint("⚠️ Skipping unknown type during encoding: \(type)")
  103. }
  104. }
  105. private enum CodingKeys: String, CodingKey {
  106. case _type
  107. }
  108. init(from decoder: Decoder) throws {
  109. let container = try decoder.container(keyedBy: CodingKeys.self)
  110. // Attempt to decode `_type` key
  111. guard let type = try? container.decode(String.self, forKey: ._type) else {
  112. debugPrint("⚠️ Missing _type in JSON entry.")
  113. self = .unknown("missing_type")
  114. return
  115. }
  116. let singleValueContainer = try decoder.singleValueContainer()
  117. switch type {
  118. case "Bolus":
  119. let bolusDTO = try singleValueContainer.decode(BolusDTO.self)
  120. self = .bolus(bolusDTO)
  121. case "TempBasal":
  122. let tempBasalDTO = try singleValueContainer.decode(TempBasalDTO.self)
  123. self = .tempBasal(tempBasalDTO)
  124. case "TempBasalDuration":
  125. let tempBasalDurationDTO = try singleValueContainer.decode(TempBasalDurationDTO.self)
  126. self = .tempBasalDuration(tempBasalDurationDTO)
  127. case "PumpSuspend":
  128. let pumpSuspendDTO = try singleValueContainer.decode(PumpSuspendDTO.self)
  129. self = .pumpSuspend(pumpSuspendDTO)
  130. default:
  131. debugPrint("⚠️ Unknown _type value: \(type)")
  132. self = .unknown(type)
  133. }
  134. }
  135. // Conformance to ImportableDTO
  136. typealias ManagedObject = PumpEventStored
  137. func store(in context: NSManagedObjectContext) -> PumpEventStored {
  138. switch self {
  139. case let .bolus(bolusDTO):
  140. let pumpEvent = PumpEventStored(context: context)
  141. pumpEvent.id = bolusDTO.id
  142. pumpEvent.timestamp = ISO8601DateFormatter().date(from: bolusDTO.timestamp)
  143. pumpEvent.type = bolusDTO._type
  144. let bolus = BolusStored(context: context)
  145. bolus.amount = NSDecimalNumber(value: bolusDTO.amount)
  146. bolus.isExternal = bolusDTO.isExternal
  147. bolus.isSMB = bolusDTO.isSMB ?? false
  148. pumpEvent.bolus = bolus
  149. return pumpEvent
  150. case let .tempBasal(tempBasalDTO):
  151. let pumpEvent = PumpEventStored(context: context)
  152. pumpEvent.id = tempBasalDTO.id
  153. pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDTO.timestamp)
  154. pumpEvent.type = tempBasalDTO._type
  155. let tempBasal = TempBasalStored(context: context)
  156. tempBasal.tempType = tempBasalDTO.temp
  157. tempBasal.rate = NSDecimalNumber(value: tempBasalDTO.rate)
  158. pumpEvent.tempBasal = tempBasal
  159. return pumpEvent
  160. case let .tempBasalDuration(tempBasalDurationDTO):
  161. let pumpEvent = PumpEventStored(context: context)
  162. pumpEvent.id = tempBasalDurationDTO.id
  163. pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDurationDTO.timestamp)
  164. pumpEvent.type = tempBasalDurationDTO._type
  165. let tempBasal = TempBasalStored(context: context)
  166. tempBasal.duration = Int16(tempBasalDurationDTO.duration)
  167. pumpEvent.tempBasal = tempBasal
  168. return pumpEvent
  169. case let .pumpSuspend(pumpSuspendDTO):
  170. let pumpEvent = PumpEventStored(context: context)
  171. pumpEvent.id = pumpSuspendDTO.id
  172. pumpEvent.timestamp = ISO8601DateFormatter().date(from: pumpSuspendDTO.timestamp)
  173. pumpEvent.type = pumpSuspendDTO._type
  174. // You can map additional pump suspend-specific fields here
  175. return pumpEvent
  176. case .unknown:
  177. debugPrint("⚠️ Skipping unknown event type.")
  178. // Return an empty PumpEventStored object or handle appropriately
  179. return PumpEventStored(context: context)
  180. }
  181. }
  182. }
  183. // Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
  184. struct BolusDTO: Codable {
  185. var id: String
  186. var timestamp: String
  187. var amount: Double
  188. var isExternal: Bool
  189. var isSMB: Bool?
  190. var duration: Int?
  191. var _type: String = "Bolus"
  192. private enum CodingKeys: String, CodingKey {
  193. case id
  194. case timestamp
  195. case amount
  196. case isExternal = "isExternalInsulin"
  197. case isSMB
  198. case duration
  199. case _type
  200. }
  201. }
  202. struct TempBasalDTO: Codable {
  203. var id: String
  204. var timestamp: String
  205. var temp: String
  206. var rate: Double
  207. var _type: String = "TempBasal"
  208. }
  209. struct TempBasalDurationDTO: Codable {
  210. var id: String
  211. var timestamp: String
  212. var duration: Int
  213. var _type: String = "TempBasalDuration"
  214. private enum CodingKeys: String, CodingKey {
  215. case id
  216. case timestamp
  217. case duration = "duration (min)"
  218. case _type
  219. }
  220. }
  221. struct PumpSuspendDTO: Codable {
  222. var id: String
  223. var timestamp: String
  224. var reason: String?
  225. var _type: String = "PumpSuspend"
  226. }
  227. // Extension with helper functions to map pump events to DTO objects via uniform masking enum
  228. extension PumpEventStored {
  229. static let dateFormatter: ISO8601DateFormatter = {
  230. let formatter = ISO8601DateFormatter()
  231. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  232. return formatter
  233. }()
  234. func toBolusDTOEnum() -> PumpEventDTO? {
  235. guard let timestamp = timestamp, let bolus = bolus, let amount = bolus.amount else {
  236. return nil
  237. }
  238. let bolusDTO = BolusDTO(
  239. id: id ?? UUID().uuidString,
  240. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  241. amount: amount.doubleValue,
  242. isExternal: bolus.isExternal,
  243. isSMB: bolus.isSMB,
  244. duration: 0
  245. )
  246. return .bolus(bolusDTO)
  247. }
  248. func toTempBasalDTOEnum() -> PumpEventDTO? {
  249. guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal, let rate = tempBasal.rate else {
  250. return nil
  251. }
  252. let tempBasalDTO = TempBasalDTO(
  253. id: "_\(id)",
  254. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  255. temp: tempBasal.tempType ?? "unknown",
  256. rate: rate.doubleValue
  257. )
  258. return .tempBasal(tempBasalDTO)
  259. }
  260. func toTempBasalDurationDTOEnum() -> PumpEventDTO? {
  261. guard let id = id, let timestamp = timestamp, let tempBasal = tempBasal else {
  262. return nil
  263. }
  264. let tempBasalDurationDTO = TempBasalDurationDTO(
  265. id: id,
  266. timestamp: PumpEventStored.dateFormatter.string(from: timestamp),
  267. duration: Int(tempBasal.duration)
  268. )
  269. return .tempBasalDuration(tempBasalDurationDTO)
  270. }
  271. }