CarbsStorage.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import CoreData
  2. import Foundation
  3. import SwiftDate
  4. import Swinject
  5. protocol CarbsObserver {
  6. func carbsDidUpdate(_ carbs: [CarbsEntry])
  7. }
  8. protocol CarbsStorage {
  9. func storeCarbs(_ carbs: [CarbsEntry]) async
  10. func syncDate() -> Date
  11. func recent() -> [CarbsEntry]
  12. func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  13. func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  14. func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
  15. }
  16. final class BaseCarbsStorage: CarbsStorage, Injectable {
  17. private let processQueue = DispatchQueue(label: "BaseCarbsStorage.processQueue")
  18. @Injected() private var storage: FileStorage!
  19. @Injected() private var broadcaster: Broadcaster!
  20. @Injected() private var settings: SettingsManager!
  21. let coredataContext = CoreDataStack.shared.newTaskContext()
  22. init(resolver: Resolver) {
  23. injectServices(resolver)
  24. }
  25. func storeCarbs(_ entries: [CarbsEntry]) async {
  26. await saveCarbEquivalents(entries: entries)
  27. await saveCarbsToCoreData(entries: entries)
  28. }
  29. /**
  30. Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap.
  31. - The function uses predefined rules to determine the duration based on the number of FPUs.
  32. - Ensures that the duration does not exceed the time cap.
  33. - Parameters:
  34. - fpus: The number of FPUs calculated from fat and protein.
  35. - timeCap: The maximum allowed duration.
  36. - Returns: The computed duration in hours.
  37. */
  38. private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
  39. switch fpus {
  40. case ..<2:
  41. return 3
  42. case 2 ..< 3:
  43. return 4
  44. case 3 ..< 4:
  45. return 5
  46. default:
  47. return timeCap
  48. }
  49. }
  50. /**
  51. Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams.
  52. - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams.
  53. - Creates future carb entries based on the adjusted carb equivalent size and interval.
  54. - Parameters:
  55. - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed.
  56. - fat: The amount of fat in the last entry.
  57. - protein: The amount of protein in the last entry.
  58. - createdAt: The creation date of the last entry.
  59. - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
  60. */
  61. private func processFPU(
  62. entries _: [CarbsEntry],
  63. fat: Decimal,
  64. protein: Decimal,
  65. createdAt: Date,
  66. actualDate: Date?
  67. ) -> ([CarbsEntry], Decimal) {
  68. let interval = settings.settings.minuteInterval
  69. let timeCap = settings.settings.timeCap
  70. let adjustment = settings.settings.individualAdjustmentFactor
  71. let delay = settings.settings.delay
  72. let kcal = protein * 4 + fat * 9
  73. let carbEquivalents = (kcal / 10) * adjustment
  74. let fpus = carbEquivalents / 10
  75. var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
  76. var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
  77. carbEquivalentSize /= Decimal(60 / interval)
  78. if carbEquivalentSize < 1.0 {
  79. carbEquivalentSize = 1.0
  80. computedDuration = Int(carbEquivalents / carbEquivalentSize)
  81. }
  82. let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
  83. carbEquivalentSize = Decimal(roundedEquivalent)
  84. var numberOfEquivalents = carbEquivalents / carbEquivalentSize
  85. var useDate = actualDate ?? createdAt
  86. let fpuID = UUID().uuidString
  87. var futureCarbArray = [CarbsEntry]()
  88. var firstIndex = true
  89. while carbEquivalents > 0, numberOfEquivalents > 0 {
  90. useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
  91. .addingTimeInterval(interval.minutes.timeInterval)
  92. firstIndex = false
  93. let eachCarbEntry = CarbsEntry(
  94. id: UUID().uuidString,
  95. createdAt: createdAt,
  96. actualDate: useDate,
  97. carbs: carbEquivalentSize,
  98. fat: 0,
  99. protein: 0,
  100. note: nil,
  101. enteredBy: CarbsEntry.manual, isFPU: true,
  102. fpuID: fpuID
  103. )
  104. futureCarbArray.append(eachCarbEntry)
  105. numberOfEquivalents -= 1
  106. }
  107. return (futureCarbArray, carbEquivalents)
  108. }
  109. private func saveCarbEquivalents(entries: [CarbsEntry]) async {
  110. guard let lastEntry = entries.last else { return }
  111. if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
  112. let (futureCarbEquivalents, carbEquivalentCount) = processFPU(
  113. entries: entries,
  114. fat: fat,
  115. protein: protein,
  116. createdAt: lastEntry.createdAt,
  117. actualDate: lastEntry.actualDate
  118. )
  119. if carbEquivalentCount > 0 {
  120. await saveFPUToCoreDataAsBatchInsert(entries: futureCarbEquivalents)
  121. }
  122. }
  123. }
  124. private func saveCarbsToCoreData(entries: [CarbsEntry]) async {
  125. guard let entry = entries.last, entry.carbs != 0 else { return }
  126. await coredataContext.perform {
  127. let newItem = CarbEntryStored(context: self.coredataContext)
  128. newItem.date = entry.actualDate ?? entry.createdAt
  129. newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
  130. newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
  131. newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
  132. newItem.id = UUID()
  133. newItem.isFPU = false
  134. newItem.isUploadedToNS = false
  135. do {
  136. guard self.coredataContext.hasChanges else { return }
  137. try self.coredataContext.save()
  138. } catch {
  139. print(error.localizedDescription)
  140. }
  141. }
  142. }
  143. private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry]) async {
  144. let commonFPUID =
  145. UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
  146. var entrySlice = ArraySlice(entries) // convert to ArraySlice
  147. let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
  148. guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
  149. let entryId = entry.id
  150. else {
  151. return true // return true to stop
  152. }
  153. carbEntry.date = entry.actualDate
  154. carbEntry.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
  155. carbEntry.id = UUID.init(uuidString: entryId)
  156. carbEntry.fpuID = commonFPUID
  157. carbEntry.isFPU = true
  158. carbEntry.isUploadedToNS = false
  159. return false // return false to continue
  160. }
  161. await coredataContext.perform {
  162. do {
  163. try self.coredataContext.execute(batchInsert)
  164. debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
  165. // Send notification for triggering a fetch in Home State Model to update the FPU Array
  166. Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
  167. } catch {
  168. debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
  169. }
  170. }
  171. }
  172. func syncDate() -> Date {
  173. Date().addingTimeInterval(-1.days.timeInterval)
  174. }
  175. func recent() -> [CarbsEntry] {
  176. storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
  177. }
  178. func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
  179. processQueue.sync {
  180. var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
  181. if fpuID != "" {
  182. if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
  183. debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
  184. } else {
  185. allValues.removeAll(where: { $0.fpuID == fpuID })
  186. storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
  187. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  188. $0.carbsDidUpdate(allValues)
  189. }
  190. }
  191. }
  192. if fpuID == "" || complex {
  193. if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
  194. debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
  195. } else {
  196. allValues.removeAll(where: { $0.id == uniqueID })
  197. storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
  198. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  199. $0.carbsDidUpdate(allValues)
  200. }
  201. }
  202. }
  203. }
  204. }
  205. func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  206. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  207. ofType: CarbEntryStored.self,
  208. onContext: coredataContext,
  209. predicate: NSPredicate.carbsNotYetUploadedToNightscout,
  210. key: "date",
  211. ascending: false
  212. )
  213. return await coredataContext.perform {
  214. return results.map { result in
  215. NightscoutTreatment(
  216. duration: nil,
  217. rawDuration: nil,
  218. rawRate: nil,
  219. absolute: nil,
  220. rate: nil,
  221. eventType: .nsCarbCorrection,
  222. createdAt: result.date,
  223. enteredBy: CarbsEntry.manual,
  224. bolus: nil,
  225. insulin: nil,
  226. carbs: Decimal(result.carbs),
  227. fat: Decimal(result.fat),
  228. protein: Decimal(result.protein),
  229. foodType: result.note,
  230. targetTop: nil,
  231. targetBottom: nil,
  232. id: result.id?.uuidString
  233. )
  234. }
  235. }
  236. }
  237. func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  238. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  239. ofType: CarbEntryStored.self,
  240. onContext: coredataContext,
  241. predicate: NSPredicate.fpusNotYetUploadedToNightscout,
  242. key: "date",
  243. ascending: false
  244. )
  245. return await coredataContext.perform {
  246. return results.map { result in
  247. NightscoutTreatment(
  248. duration: nil,
  249. rawDuration: nil,
  250. rawRate: nil,
  251. absolute: nil,
  252. rate: nil,
  253. eventType: .nsCarbCorrection,
  254. createdAt: result.date,
  255. enteredBy: CarbsEntry.manual,
  256. bolus: nil,
  257. insulin: nil,
  258. carbs: Decimal(result.carbs),
  259. fat: Decimal(result.fat),
  260. protein: Decimal(result.protein),
  261. foodType: result.note,
  262. targetTop: nil,
  263. targetBottom: nil,
  264. id: result.fpuID?.uuidString
  265. )
  266. }
  267. }
  268. }
  269. }