CarbsStorage.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 storeCarbEquivalents(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(entries _: [CarbsEntry], fat: Decimal, protein: Decimal, createdAt: Date) -> ([CarbsEntry], Decimal) {
  62. let interval = settings.settings.minuteInterval
  63. let timeCap = settings.settings.timeCap
  64. let adjustment = settings.settings.individualAdjustmentFactor
  65. let delay = settings.settings.delay
  66. let kcal = protein * 4 + fat * 9
  67. let carbEquivalents = (kcal / 10) * adjustment
  68. let fpus = carbEquivalents / 10
  69. var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
  70. var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
  71. carbEquivalentSize /= Decimal(60 / interval)
  72. if carbEquivalentSize < 1.0 {
  73. carbEquivalentSize = 1.0
  74. computedDuration = Int(carbEquivalents / carbEquivalentSize)
  75. }
  76. let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
  77. carbEquivalentSize = Decimal(roundedEquivalent)
  78. var numberOfEquivalents = carbEquivalents / carbEquivalentSize
  79. var useDate = createdAt
  80. let fpuID = UUID().uuidString
  81. var futureCarbArray = [CarbsEntry]()
  82. var firstIndex = true
  83. while carbEquivalents > 0, numberOfEquivalents > 0 {
  84. useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
  85. .addingTimeInterval(interval.minutes.timeInterval)
  86. firstIndex = false
  87. let eachCarbEntry = CarbsEntry(
  88. id: UUID().uuidString,
  89. createdAt: createdAt,
  90. actualDate: useDate,
  91. carbs: carbEquivalentSize,
  92. fat: 0,
  93. protein: 0,
  94. note: nil,
  95. enteredBy: CarbsEntry.manual, isFPU: true,
  96. fpuID: fpuID
  97. )
  98. futureCarbArray.append(eachCarbEntry)
  99. numberOfEquivalents -= 1
  100. }
  101. return (futureCarbArray, carbEquivalents)
  102. }
  103. private func storeCarbEquivalents(entries: [CarbsEntry]) async {
  104. guard let lastEntry = entries.last else { return }
  105. if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
  106. let (futureCarbEquivalents, carbEquivalentCount) = processFPU(
  107. entries: entries,
  108. fat: fat,
  109. protein: protein,
  110. createdAt: lastEntry.createdAt
  111. )
  112. if carbEquivalentCount > 0 {
  113. await saveFPUToCoreDataAsBatchInsert(entries: futureCarbEquivalents)
  114. }
  115. }
  116. }
  117. private func saveCarbsToCoreData(entries: [CarbsEntry]) async {
  118. guard let entry = entries.last, entry.carbs != 0 else { return }
  119. await coredataContext.perform {
  120. let newItem = CarbEntryStored(context: self.coredataContext)
  121. newItem.date = entry.actualDate ?? entry.createdAt
  122. newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
  123. newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
  124. newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
  125. newItem.id = UUID()
  126. newItem.isFPU = false
  127. newItem.isUploadedToNS = false
  128. do {
  129. guard self.coredataContext.hasChanges else { return }
  130. try self.coredataContext.save()
  131. } catch {
  132. print(error.localizedDescription)
  133. }
  134. }
  135. }
  136. private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry]) async {
  137. let commonFPUID =
  138. UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
  139. var entrySlice = ArraySlice(entries) // convert to ArraySlice
  140. let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
  141. guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
  142. let entryId = entry.id
  143. else {
  144. return true // return true to stop
  145. }
  146. carbEntry.date = entry.actualDate
  147. carbEntry.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
  148. carbEntry.id = UUID.init(uuidString: entryId)
  149. carbEntry.fpuID = commonFPUID
  150. carbEntry.isFPU = true
  151. carbEntry.isUploadedToNS = false
  152. return false // return false to continue
  153. }
  154. await coredataContext.perform {
  155. do {
  156. try self.coredataContext.execute(batchInsert)
  157. debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
  158. } catch {
  159. debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
  160. }
  161. }
  162. }
  163. func syncDate() -> Date {
  164. Date().addingTimeInterval(-1.days.timeInterval)
  165. }
  166. func recent() -> [CarbsEntry] {
  167. storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
  168. }
  169. func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
  170. processQueue.sync {
  171. var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
  172. if fpuID != "" {
  173. if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
  174. debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
  175. } else {
  176. allValues.removeAll(where: { $0.fpuID == fpuID })
  177. storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
  178. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  179. $0.carbsDidUpdate(allValues)
  180. }
  181. }
  182. }
  183. if fpuID == "" || complex {
  184. if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
  185. debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
  186. } else {
  187. allValues.removeAll(where: { $0.id == uniqueID })
  188. storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
  189. broadcaster.notify(CarbsObserver.self, on: processQueue) {
  190. $0.carbsDidUpdate(allValues)
  191. }
  192. }
  193. }
  194. }
  195. }
  196. func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  197. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  198. ofType: CarbEntryStored.self,
  199. onContext: coredataContext,
  200. predicate: NSPredicate.carbsNotYetUploadedToNightscout,
  201. key: "date",
  202. ascending: false
  203. )
  204. return await coredataContext.perform {
  205. return results.map { result in
  206. NightscoutTreatment(
  207. duration: nil,
  208. rawDuration: nil,
  209. rawRate: nil,
  210. absolute: nil,
  211. rate: nil,
  212. eventType: .nsCarbCorrection,
  213. createdAt: result.date,
  214. enteredBy: CarbsEntry.manual,
  215. bolus: nil,
  216. insulin: nil,
  217. carbs: Decimal(result.carbs),
  218. fat: Decimal(result.fat),
  219. protein: Decimal(result.protein),
  220. foodType: result.note,
  221. targetTop: nil,
  222. targetBottom: nil,
  223. id: result.id?.uuidString
  224. )
  225. }
  226. }
  227. }
  228. func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  229. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  230. ofType: CarbEntryStored.self,
  231. onContext: coredataContext,
  232. predicate: NSPredicate.fpusNotYetUploadedToNightscout,
  233. key: "date",
  234. ascending: false
  235. )
  236. return await coredataContext.perform {
  237. return results.map { result in
  238. NightscoutTreatment(
  239. duration: nil,
  240. rawDuration: nil,
  241. rawRate: nil,
  242. absolute: nil,
  243. rate: nil,
  244. eventType: .nsCarbCorrection,
  245. createdAt: result.date,
  246. enteredBy: CarbsEntry.manual,
  247. bolus: nil,
  248. insulin: nil,
  249. carbs: Decimal(result.carbs),
  250. fat: Decimal(result.fat),
  251. protein: Decimal(result.protein),
  252. foodType: result.note,
  253. targetTop: nil,
  254. targetBottom: nil,
  255. id: result.fpuID?.uuidString
  256. )
  257. }
  258. }
  259. }
  260. }