| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- import Combine
- import CoreData
- import Foundation
- import SwiftDate
- import Swinject
- protocol CarbsObserver {
- func carbsDidUpdate(_ carbs: [CarbsEntry])
- }
- protocol CarbsStorage {
- var updatePublisher: AnyPublisher<Void, Never> { get }
- func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
- func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
- func syncDate() -> Date
- func recent() -> [CarbsEntry]
- func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
- func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
- func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
- }
- final class BaseCarbsStorage: CarbsStorage, Injectable {
- private let processQueue = DispatchQueue(label: "BaseCarbsStorage.processQueue")
- @Injected() private var storage: FileStorage!
- @Injected() private var broadcaster: Broadcaster!
- @Injected() private var settings: SettingsManager!
- let coredataContext = CoreDataStack.shared.newTaskContext()
- private let updateSubject = PassthroughSubject<Void, Never>()
- var updatePublisher: AnyPublisher<Void, Never> {
- updateSubject.eraseToAnyPublisher()
- }
- init(resolver: Resolver) {
- injectServices(resolver)
- }
- func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
- var entriesToStore = entries
- if areFetchedFromRemote {
- entriesToStore = await filterRemoteEntries(entries: entriesToStore)
- }
- await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
- await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
- }
- private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
- // Fetch only the date property from Core Data
- guard let existing24hCarbEntries = await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: CarbEntryStored.self,
- onContext: coredataContext,
- predicate: NSPredicate.predicateForOneDayAgo,
- key: "date",
- ascending: false,
- batchSize: 50,
- propertiesToFetch: ["date", "objectID"]
- ) as? [[String: Any]] else {
- return entries
- }
- // Extract dates into a set for efficient lookup
- let existingTimestamps = Set(existing24hCarbEntries.compactMap { $0["date"] as? Date })
- // Remove all entries that have a matching date in existingTimestamps
- var filteredEntries = entries
- filteredEntries.removeAll { entry in
- let entryDate = entry.actualDate ?? entry.createdAt
- return existingTimestamps.contains(entryDate)
- }
- return filteredEntries
- }
- /**
- Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap.
- - The function uses predefined rules to determine the duration based on the number of FPUs.
- - Ensures that the duration does not exceed the time cap.
- - Parameters:
- - fpus: The number of FPUs calculated from fat and protein.
- - timeCap: The maximum allowed duration.
- - Returns: The computed duration in hours.
- */
- private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
- switch fpus {
- case ..<2:
- return 3
- case 2 ..< 3:
- return 4
- case 3 ..< 4:
- return 5
- default:
- return timeCap
- }
- }
- /**
- Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams.
- - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams.
- - Creates future carb entries based on the adjusted carb equivalent size and interval.
- - Parameters:
- - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed.
- - fat: The amount of fat in the last entry.
- - protein: The amount of protein in the last entry.
- - createdAt: The creation date of the last entry.
- - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
- */
- private func processFPU(
- entries _: [CarbsEntry],
- fat: Decimal,
- protein: Decimal,
- createdAt: Date,
- actualDate: Date?
- ) -> ([CarbsEntry], Decimal) {
- let interval = settings.settings.minuteInterval
- let timeCap = settings.settings.timeCap
- let adjustment = settings.settings.individualAdjustmentFactor
- let delay = settings.settings.delay
- let kcal = protein * 4 + fat * 9
- let carbEquivalents = (kcal / 10) * adjustment
- let fpus = carbEquivalents / 10
- var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
- var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
- carbEquivalentSize /= Decimal(60 / interval)
- if carbEquivalentSize < 1.0 {
- carbEquivalentSize = 1.0
- computedDuration = Int(carbEquivalents / carbEquivalentSize)
- }
- let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
- carbEquivalentSize = Decimal(roundedEquivalent)
- var numberOfEquivalents = carbEquivalents / carbEquivalentSize
- var useDate = actualDate ?? createdAt
- let fpuID = UUID().uuidString
- var futureCarbArray = [CarbsEntry]()
- var firstIndex = true
- while carbEquivalents > 0, numberOfEquivalents > 0 {
- useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
- .addingTimeInterval(interval.minutes.timeInterval)
- firstIndex = false
- let eachCarbEntry = CarbsEntry(
- id: UUID().uuidString,
- createdAt: createdAt,
- actualDate: useDate,
- carbs: carbEquivalentSize,
- fat: 0,
- protein: 0,
- note: nil,
- enteredBy: CarbsEntry.manual, isFPU: true,
- fpuID: fpuID
- )
- futureCarbArray.append(eachCarbEntry)
- numberOfEquivalents -= 1
- }
- return (futureCarbArray, carbEquivalents)
- }
- private func saveCarbEquivalents(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
- guard let lastEntry = entries.last else { return }
- if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
- let (futureCarbEquivalents, carbEquivalentCount) = processFPU(
- entries: entries,
- fat: fat,
- protein: protein,
- createdAt: lastEntry.createdAt,
- actualDate: lastEntry.actualDate
- )
- if carbEquivalentCount > 0 {
- await saveFPUToCoreDataAsBatchInsert(entries: futureCarbEquivalents, areFetchedFromRemote: areFetchedFromRemote)
- }
- }
- }
- private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
- guard let entry = entries.last, entry.carbs != 0 else { return }
- await coredataContext.perform {
- let newItem = CarbEntryStored(context: self.coredataContext)
- newItem.date = entry.actualDate ?? entry.createdAt
- newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
- newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
- newItem.protein = Double(truncating: NSDecimalNumber(decimal: entry.protein ?? 0))
- newItem.note = entry.note
- newItem.id = UUID()
- newItem.isFPU = false
- newItem.isUploadedToNS = areFetchedFromRemote ? true : false
- do {
- guard self.coredataContext.hasChanges else { return }
- try self.coredataContext.save()
- } catch {
- print(error.localizedDescription)
- }
- }
- }
- private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
- let commonFPUID =
- UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
- var entrySlice = ArraySlice(entries) // convert to ArraySlice
- let batchInsert = NSBatchInsertRequest(entity: CarbEntryStored.entity()) { (managedObject: NSManagedObject) -> Bool in
- guard let carbEntry = managedObject as? CarbEntryStored, let entry = entrySlice.popFirst(),
- let entryId = entry.id
- else {
- return true // return true to stop
- }
- carbEntry.date = entry.actualDate
- carbEntry.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
- carbEntry.id = UUID.init(uuidString: entryId)
- carbEntry.fpuID = commonFPUID
- carbEntry.isFPU = true
- carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
- return false // return false to continue
- }
- await coredataContext.perform {
- do {
- try self.coredataContext.execute(batchInsert)
- debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
- // Notify subscriber in Home State Model to update the FPU Array
- self.updateSubject.send(())
- } catch {
- debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
- }
- }
- }
- func syncDate() -> Date {
- Date().addingTimeInterval(-1.days.timeInterval)
- }
- func recent() -> [CarbsEntry] {
- storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
- }
- func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
- let taskContext = CoreDataStack.shared.newTaskContext()
- taskContext.name = "deleteContext"
- taskContext.transactionAuthor = "deleteCarbs"
- var carbEntry: CarbEntryStored?
- await taskContext.perform {
- do {
- carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
- guard let carbEntry = carbEntry else {
- debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
- return
- }
- if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
- // fetch request for all carb entries with the same id
- let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
- fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
- // NSBatchDeleteRequest
- let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
- deleteRequest.resultType = .resultTypeCount
- // execute the batch delete request
- let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
- debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
- // Notifiy subscribers of the batch delete
- self.updateSubject.send(())
- } else {
- taskContext.delete(carbEntry)
- guard taskContext.hasChanges else { return }
- try taskContext.save()
- debugPrint(
- "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
- )
- }
- } catch {
- debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
- }
- }
- }
- func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
- processQueue.sync {
- var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
- if fpuID != "" {
- if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
- debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
- } else {
- allValues.removeAll(where: { $0.fpuID == fpuID })
- storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
- broadcaster.notify(CarbsObserver.self, on: processQueue) {
- $0.carbsDidUpdate(allValues)
- }
- }
- }
- if fpuID == "" || complex {
- if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
- debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
- } else {
- allValues.removeAll(where: { $0.id == uniqueID })
- storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
- broadcaster.notify(CarbsObserver.self, on: processQueue) {
- $0.carbsDidUpdate(allValues)
- }
- }
- }
- }
- }
- func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
- let results = await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: CarbEntryStored.self,
- onContext: coredataContext,
- predicate: NSPredicate.carbsNotYetUploadedToNightscout,
- key: "date",
- ascending: false
- )
- guard let carbEntries = results as? [CarbEntryStored] else {
- return []
- }
- return await coredataContext.perform {
- return carbEntries.map { result in
- NightscoutTreatment(
- duration: nil,
- rawDuration: nil,
- rawRate: nil,
- absolute: nil,
- rate: nil,
- eventType: .nsCarbCorrection,
- createdAt: result.date,
- enteredBy: CarbsEntry.manual,
- bolus: nil,
- insulin: nil,
- notes: result.note,
- carbs: Decimal(result.carbs),
- fat: Decimal(result.fat),
- protein: Decimal(result.protein),
- foodType: result.note,
- targetTop: nil,
- targetBottom: nil,
- id: result.id?.uuidString
- )
- }
- }
- }
- func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
- let results = await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: CarbEntryStored.self,
- onContext: coredataContext,
- predicate: NSPredicate.fpusNotYetUploadedToNightscout,
- key: "date",
- ascending: false
- )
- guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
- return await coredataContext.perform {
- return fpuEntries.map { result in
- NightscoutTreatment(
- duration: nil,
- rawDuration: nil,
- rawRate: nil,
- absolute: nil,
- rate: nil,
- eventType: .nsCarbCorrection,
- createdAt: result.date,
- enteredBy: CarbsEntry.manual,
- bolus: nil,
- insulin: nil,
- carbs: Decimal(result.carbs),
- fat: Decimal(result.fat),
- protein: Decimal(result.protein),
- foodType: result.note,
- targetTop: nil,
- targetBottom: nil,
- id: result.fpuID?.uuidString
- )
- }
- }
- }
- }
|