CarbsStorage.swift 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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])
  10. func syncDate() -> Date
  11. func recent() -> [CarbsEntry]
  12. func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment]
  13. func deleteCarbs(at date: Date)
  14. }
  15. final class BaseCarbsStorage: CarbsStorage, Injectable {
  16. private let processQueue = DispatchQueue(label: "BaseCarbsStorage.processQueue")
  17. @Injected() private var storage: FileStorage!
  18. @Injected() private var broadcaster: Broadcaster!
  19. @Injected() private var settings: SettingsManager!
  20. let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
  21. init(resolver: Resolver) {
  22. injectServices(resolver)
  23. }
  24. /**
  25. Processes and stores carbohydrate entries, including handling entries with fat and protein to calculate and distribute future carb equivalents.
  26. - The function processes fat and protein units (FPUs) by creating carb equivalents for future use.
  27. - Ensures each carb equivalent is at least 1.0 grams by adjusting the interval if necessary.
  28. - Stores the actual carbohydrate entries.
  29. - Saves the data to CoreData for statistical purposes.
  30. - Notifies observers of the carbohydrate data update.
  31. - Parameters:
  32. - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed and stored.
  33. */
  34. func storeCarbs(_ entries: [CarbsEntry]) {
  35. processQueue.sync {
  36. let file = OpenAPS.Monitor.carbHistory
  37. var entriesToStore: [CarbsEntry] = []
  38. guard let lastEntry = entries.last else { return }
  39. if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
  40. let (futureCarbArray, carbEquivalents) = processFPU(
  41. entries: entries,
  42. fat: fat,
  43. protein: protein,
  44. createdAt: lastEntry.createdAt
  45. )
  46. if carbEquivalents > 0 {
  47. self.storage.transaction { storage in
  48. storage.append(futureCarbArray, to: file, uniqBy: \.id)
  49. entriesToStore = storage.retrieve(file, as: [CarbsEntry].self)?
  50. .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
  51. .sorted { $0.createdAt > $1.createdAt } ?? []
  52. storage.save(Array(entriesToStore), as: file)
  53. }
  54. }
  55. }
  56. if lastEntry.carbs > 0 {
  57. self.storage.transaction { storage in
  58. storage.append(entries, to: file, uniqBy: \.createdAt)
  59. entriesToStore = storage.retrieve(file, as: [CarbsEntry].self)?
  60. .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
  61. .sorted { $0.createdAt > $1.createdAt } ?? []
  62. storage.save(Array(entriesToStore), as: file)
  63. }
  64. }
  65. var cbs: Decimal = 0
  66. var carbDate = Date()
  67. if entries.isNotEmpty {
  68. cbs = entries[0].carbs
  69. carbDate = entries[0].createdAt
  70. }
  71. if cbs != 0 {
  72. self.coredataContext.perform {
  73. let carbDataForStats = Carbohydrates(context: self.coredataContext)
  74. carbDataForStats.date = carbDate
  75. carbDataForStats.carbs = cbs as NSDecimalNumber
  76. try? self.coredataContext.save()
  77. }
  78. }
  79. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  80. $0.carbsDidUpdate(entriesToStore)
  81. }
  82. }
  83. }
  84. /**
  85. Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap.
  86. - The function uses predefined rules to determine the duration based on the number of FPUs.
  87. - Ensures that the duration does not exceed the time cap.
  88. - Parameters:
  89. - fpus: The number of FPUs calculated from fat and protein.
  90. - timeCap: The maximum allowed duration.
  91. - Returns: The computed duration in hours.
  92. */
  93. private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
  94. switch fpus {
  95. case ..<2:
  96. return 3
  97. case 2 ..< 3:
  98. return 4
  99. case 3 ..< 4:
  100. return 5
  101. default:
  102. return timeCap
  103. }
  104. }
  105. /**
  106. Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams.
  107. - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams.
  108. - Creates future carb entries based on the adjusted carb equivalent size and interval.
  109. - Parameters:
  110. - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed.
  111. - fat: The amount of fat in the last entry.
  112. - protein: The amount of protein in the last entry.
  113. - createdAt: The creation date of the last entry.
  114. - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
  115. */
  116. private func processFPU(entries _: [CarbsEntry], fat: Decimal, protein: Decimal, createdAt: Date) -> ([CarbsEntry], Decimal) {
  117. let interval = settings.settings.minuteInterval
  118. let timeCap = settings.settings.timeCap
  119. let adjustment = settings.settings.individualAdjustmentFactor
  120. let delay = settings.settings.delay
  121. let kcal = protein * 4 + fat * 9
  122. let carbEquivalents = (kcal / 10) * adjustment
  123. let fpus = carbEquivalents / 10
  124. var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
  125. var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
  126. carbEquivalentSize /= Decimal(60 / interval)
  127. if carbEquivalentSize < 1.0 {
  128. carbEquivalentSize = 1.0
  129. computedDuration = Int(carbEquivalents / carbEquivalentSize)
  130. }
  131. let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
  132. carbEquivalentSize = Decimal(roundedEquivalent)
  133. var numberOfEquivalents = carbEquivalents / carbEquivalentSize
  134. var useDate = createdAt
  135. let fpuID = UUID().uuidString
  136. var futureCarbArray = [CarbsEntry]()
  137. var firstIndex = true
  138. while carbEquivalents > 0, numberOfEquivalents > 0 {
  139. useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
  140. .addingTimeInterval(interval.minutes.timeInterval)
  141. firstIndex = false
  142. let eachCarbEntry = CarbsEntry(
  143. id: UUID().uuidString, createdAt: useDate,
  144. carbs: carbEquivalentSize, fat: 0, protein: 0, note: nil,
  145. enteredBy: CarbsEntry.manual, isFPU: true,
  146. fpuID: fpuID
  147. )
  148. futureCarbArray.append(eachCarbEntry)
  149. numberOfEquivalents -= 1
  150. }
  151. return (futureCarbArray, carbEquivalents)
  152. }
  153. func syncDate() -> Date {
  154. Date().addingTimeInterval(-1.days.timeInterval)
  155. }
  156. func recent() -> [CarbsEntry] {
  157. storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
  158. }
  159. func deleteCarbs(at date: Date) {
  160. processQueue.sync {
  161. var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
  162. guard let entryIndex = allValues.firstIndex(where: { $0.createdAt == date }) else {
  163. return
  164. }
  165. // If deleteing a FPUs remove all of those with the same ID
  166. if allValues[entryIndex].isFPU != nil, allValues[entryIndex].isFPU ?? false {
  167. let fpuString = allValues[entryIndex].fpuID
  168. allValues.removeAll(where: { $0.fpuID == fpuString })
  169. storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
  170. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  171. $0.carbsDidUpdate(allValues)
  172. }
  173. } else {
  174. allValues.remove(at: entryIndex)
  175. storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
  176. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  177. $0.carbsDidUpdate(allValues)
  178. }
  179. }
  180. }
  181. }
  182. func nightscoutTretmentsNotUploaded() -> [NightscoutTreatment] {
  183. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NightscoutTreatment].self) ?? []
  184. let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
  185. let treatments = eventsManual.map {
  186. NightscoutTreatment(
  187. duration: nil,
  188. rawDuration: nil,
  189. rawRate: nil,
  190. absolute: nil,
  191. rate: nil,
  192. eventType: .nsCarbCorrection,
  193. createdAt: $0.createdAt,
  194. enteredBy: CarbsEntry.manual,
  195. bolus: nil,
  196. insulin: nil,
  197. carbs: $0.carbs,
  198. fat: $0.fat,
  199. protein: $0.protein,
  200. foodType: $0.note,
  201. targetTop: nil,
  202. targetBottom: nil
  203. )
  204. }
  205. return Array(Set(treatments).subtracting(Set(uploaded)))
  206. }
  207. }