DeterminationStorage.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import Swinject
  5. protocol DeterminationStorage {
  6. func fetchLastDeterminationObjectID(predicate: NSPredicate) async throws -> [NSManagedObjectID]
  7. func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
  8. func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
  9. func fetchForecastObjects(
  10. for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
  11. in context: NSManagedObjectContext
  12. ) async -> (UUID, Forecast?, [ForecastValue])
  13. func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
  14. func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
  15. async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
  16. }
  17. final class BaseDeterminationStorage: DeterminationStorage, Injectable {
  18. private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  19. private let context: NSManagedObjectContext
  20. init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
  21. self.context = context ?? CoreDataStack.shared.newTaskContext()
  22. injectServices(resolver)
  23. }
  24. func fetchLastDeterminationObjectID(predicate: NSPredicate) async throws -> [NSManagedObjectID] {
  25. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  26. ofType: OrefDetermination.self,
  27. onContext: context,
  28. predicate: predicate,
  29. key: "deliverAt",
  30. ascending: false,
  31. fetchLimit: 1
  32. )
  33. return try await context.perform {
  34. guard let fetchedResults = results as? [OrefDetermination] else {
  35. throw CoreDataError.fetchError(function: #function, file: #file)
  36. }
  37. return fetchedResults.map(\.objectID)
  38. }
  39. }
  40. func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID] {
  41. await context.perform {
  42. do {
  43. guard let determination = try context.existingObject(with: determinationID) as? OrefDetermination,
  44. let forecastSet = determination.forecasts
  45. else {
  46. return []
  47. }
  48. let forecasts = Array(forecastSet)
  49. return forecasts.map(\.objectID) as [NSManagedObjectID]
  50. } catch {
  51. debugPrint(
  52. "Failed \(DebuggingIdentifiers.failed) to fetch Forecast IDs for OrefDetermination with ID \(determinationID): \(error.localizedDescription)"
  53. )
  54. return []
  55. }
  56. }
  57. }
  58. func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID] {
  59. await context.perform {
  60. do {
  61. guard let forecast = try context.existingObject(with: forecastID) as? Forecast,
  62. let forecastValueSet = forecast.forecastValues
  63. else {
  64. return []
  65. }
  66. let forecastValues = forecastValueSet.sorted(by: { $0.index < $1.index })
  67. return forecastValues.map(\.objectID)
  68. } catch {
  69. debugPrint(
  70. "Failed \(DebuggingIdentifiers.failed) to fetch Forecast Value IDs with ID \(forecastID): \(error.localizedDescription)"
  71. )
  72. return []
  73. }
  74. }
  75. }
  76. // Fetch forecast objects for a given data set
  77. func fetchForecastObjects(
  78. for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
  79. in context: NSManagedObjectContext
  80. ) async -> (UUID, Forecast?, [ForecastValue]) {
  81. return await context.perform {
  82. var forecast: Forecast?
  83. var forecastValues: [ForecastValue] = []
  84. do {
  85. // Fetch the forecast object
  86. forecast = try context.existingObject(with: data.forecastID) as? Forecast
  87. // Fetch the first 3h of forecast values
  88. for forecastValueID in data.forecastValueIDs.prefix(36) {
  89. if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
  90. forecastValues.append(forecastValue)
  91. }
  92. }
  93. } catch {
  94. debugPrint(
  95. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
  96. )
  97. }
  98. return (data.id, forecast, forecastValues)
  99. }
  100. }
  101. // Convert NSDecimalNumber to Decimal
  102. func decimal(from nsDecimalNumber: NSDecimalNumber?) -> Decimal {
  103. nsDecimalNumber?.decimalValue ?? 0.0
  104. }
  105. // Convert NSSet to array of Ints for Predictions
  106. func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
  107. let forecastIDs = await getForecastIDs(for: determinationID, in: context)
  108. var forecastValuesList: [Int] = []
  109. for forecastID in forecastIDs {
  110. await context.perform {
  111. if let forecast = try? self.context.existingObject(with: forecastID) as? Forecast {
  112. // Filter the forecast based on the type
  113. if forecast.type == type {
  114. let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
  115. for forecastValueID in forecastValueIDs {
  116. if let forecastValue = try? self.context
  117. .existingObject(with: forecastValueID) as? ForecastValue
  118. {
  119. let forecastValueInt = Int(forecastValue.value)
  120. forecastValuesList.append(forecastValueInt)
  121. }
  122. }
  123. }
  124. }
  125. }
  126. }
  127. return forecastValuesList.isEmpty ? nil : forecastValuesList
  128. }
  129. func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination? {
  130. var result: Determination?
  131. guard let determinationId = determinationIds.first else {
  132. return nil
  133. }
  134. let predictions = Predictions(
  135. iob: await parseForecastValues(ofType: "iob", from: determinationId),
  136. zt: await parseForecastValues(ofType: "zt", from: determinationId),
  137. cob: await parseForecastValues(ofType: "cob", from: determinationId),
  138. uam: await parseForecastValues(ofType: "uam", from: determinationId)
  139. )
  140. return await context.perform {
  141. do {
  142. let orefDetermination = try self.context.existingObject(with: determinationId) as? OrefDetermination
  143. // Check if the fetched object is of the expected type
  144. if let orefDetermination = orefDetermination {
  145. result = Determination(
  146. id: orefDetermination.id ?? UUID(),
  147. reason: orefDetermination.reason ?? "",
  148. units: orefDetermination.smbToDeliver as Decimal?,
  149. insulinReq: self.decimal(from: orefDetermination.insulinReq),
  150. eventualBG: orefDetermination.eventualBG as? Int,
  151. sensitivityRatio: self.decimal(from: orefDetermination.sensitivityRatio),
  152. rate: self.decimal(from: orefDetermination.rate),
  153. duration: self.decimal(from: orefDetermination.duration),
  154. iob: self.decimal(from: orefDetermination.iob),
  155. cob: Decimal(orefDetermination.cob),
  156. predictions: predictions,
  157. deliverAt: orefDetermination.deliverAt,
  158. carbsReq: orefDetermination.carbsRequired != 0 ? Decimal(orefDetermination.carbsRequired) : nil,
  159. temp: TempType(rawValue: orefDetermination.temp ?? "absolute"),
  160. bg: self.decimal(from: orefDetermination.glucose),
  161. reservoir: self.decimal(from: orefDetermination.reservoir),
  162. isf: self.decimal(from: orefDetermination.insulinSensitivity),
  163. timestamp: orefDetermination.timestamp,
  164. tdd: self.decimal(from: orefDetermination.totalDailyDose),
  165. insulin: nil,
  166. current_target: self.decimal(from: orefDetermination.currentTarget),
  167. insulinForManualBolus: self.decimal(from: orefDetermination.insulinForManualBolus),
  168. manualBolusErrorString: self.decimal(from: orefDetermination.manualBolusErrorString),
  169. minDelta: self.decimal(from: orefDetermination.minDelta),
  170. expectedDelta: self.decimal(from: orefDetermination.expectedDelta),
  171. minGuardBG: nil,
  172. minPredBG: nil,
  173. threshold: self.decimal(from: orefDetermination.threshold),
  174. carbRatio: self.decimal(from: orefDetermination.carbRatio),
  175. received: orefDetermination.enacted // this is actually part of NS...
  176. )
  177. }
  178. } catch {
  179. debugPrint(
  180. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch managed object with error: \(error.localizedDescription)"
  181. )
  182. }
  183. return result
  184. }
  185. }
  186. func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
  187. async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
  188. {
  189. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  190. ofType: Forecast.self,
  191. onContext: context,
  192. predicate: NSPredicate(format: "orefDetermination = %@", determinationID),
  193. key: "type",
  194. ascending: true,
  195. relationshipKeyPathsForPrefetching: ["forecastValues"]
  196. )
  197. var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
  198. await context.perform {
  199. if let forecasts = results as? [Forecast] {
  200. for forecast in forecasts {
  201. // Use the helper property that already sorts by index
  202. let sortedValues = forecast.forecastValuesArray
  203. result.append((
  204. id: UUID(),
  205. forecastID: forecast.objectID,
  206. forecastValueIDs: sortedValues.map(\.objectID)
  207. ))
  208. }
  209. }
  210. }
  211. return result
  212. }
  213. }