DataTableStateModel.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import CoreData
  2. import HealthKit
  3. import Observation
  4. import SwiftUI
  5. extension DataTable {
  6. @Observable final class StateModel: BaseStateModel<Provider> {
  7. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  8. @ObservationIgnored @Injected() var apsManager: APSManager!
  9. @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
  10. @ObservationIgnored @Injected() private var storage: FileStorage!
  11. @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
  12. @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
  13. @ObservationIgnored @Injected() var healthKitManager: HealthKitManager!
  14. @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
  15. let coredataContext = CoreDataStack.shared.newTaskContext()
  16. var mode: Mode = .treatments
  17. var treatments: [Treatment] = []
  18. var glucose: [Glucose] = []
  19. var meals: [Treatment] = []
  20. var manualGlucose: Decimal = 0
  21. var waitForSuggestion: Bool = false
  22. var insulinEntryDeleted: Bool = false
  23. var carbEntryDeleted: Bool = false
  24. var units: GlucoseUnits = .mgdL
  25. var carbEntryToEdit: CarbEntryStored?
  26. var showCarbEntryEditor = false
  27. override func subscribe() {
  28. units = settingsManager.settings.units
  29. broadcaster.register(DeterminationObserver.self, observer: self)
  30. broadcaster.register(SettingsObserver.self, observer: self)
  31. }
  32. func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
  33. glucoseStorage.isGlucoseDataFresh(glucoseDate)
  34. }
  35. // Glucose deletion from history and from remote services
  36. /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  37. func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  38. Task {
  39. await deleteGlucose(treatmentObjectID)
  40. }
  41. }
  42. func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
  43. // Delete from Apple Health/Tidepool
  44. await deleteGlucoseFromServices(treatmentObjectID)
  45. // Delete from Core Data
  46. await glucoseStorage.deleteGlucose(treatmentObjectID)
  47. }
  48. func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
  49. let taskContext = CoreDataStack.shared.newTaskContext()
  50. taskContext.name = "deleteContext"
  51. taskContext.transactionAuthor = "deleteGlucoseFromServices"
  52. await taskContext.perform {
  53. do {
  54. let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
  55. guard let glucoseToDelete = result else {
  56. debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
  57. return
  58. }
  59. // Delete from Nightscout
  60. if let id = glucoseToDelete.id?.uuidString {
  61. self.provider.deleteManualGlucoseFromNightscout(withID: id)
  62. }
  63. // Delete from Apple Health
  64. if let id = glucoseToDelete.id?.uuidString {
  65. self.provider.deleteGlucoseFromHealth(withSyncID: id)
  66. }
  67. debugPrint(
  68. "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
  69. )
  70. } catch {
  71. debugPrint(
  72. "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
  73. )
  74. }
  75. }
  76. }
  77. func addManualGlucose() {
  78. // Always save value in mg/dL
  79. let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
  80. let glucoseAsInt = Int(glucose)
  81. glucoseStorage.addManualGlucose(glucose: glucoseAsInt)
  82. }
  83. // Carb and FPU deletion from history
  84. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  85. func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  86. Task {
  87. await deleteCarbs(treatmentObjectID)
  88. await MainActor.run {
  89. carbEntryDeleted = true
  90. waitForSuggestion = true
  91. }
  92. }
  93. }
  94. func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
  95. // Delete from Apple Health/Tidepool
  96. await deleteCarbsFromServices(treatmentObjectID)
  97. // Delete carbs from Core Data
  98. await carbsStorage.deleteCarbs(treatmentObjectID)
  99. // Perform a determine basal sync to update cob
  100. await apsManager.determineBasalSync()
  101. }
  102. func updateCarbEntry(_ treatmentObjectID: NSManagedObjectID, newAmount: Decimal, newNote: String) {
  103. Task {
  104. // Update carb entry in Core Data
  105. await updateCarbEntryInCoreData(treatmentObjectID, newAmount: newAmount, newNote: newNote)
  106. // Perform a determine basal sync to keep data up to date
  107. await apsManager.determineBasalSync()
  108. // Delete carbs from Services
  109. await deleteCarbsFromServices(treatmentObjectID)
  110. // Upload updated carb entry to services in parallel
  111. async let nightscoutUpload: () = self.provider.nightscoutManager.uploadCarbs()
  112. async let healthKitUpload: () = self.provider.healthkitManager.uploadCarbs()
  113. async let tidepoolUpload: () = self.provider.tidepoolManager.uploadCarbs()
  114. // Wait for all uploads to complete
  115. _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
  116. }
  117. }
  118. private func updateCarbEntryInCoreData(
  119. _ treatmentObjectID: NSManagedObjectID,
  120. newAmount: Decimal,
  121. newNote: String
  122. ) async {
  123. let context = CoreDataStack.shared.newTaskContext()
  124. context.name = "updateContext"
  125. context.transactionAuthor = "updateCarbEntry"
  126. await context.perform {
  127. do {
  128. if let carbToUpdate = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored {
  129. carbToUpdate.carbs = Double(newAmount)
  130. carbToUpdate.note = newNote
  131. carbToUpdate.isUploadedToNS = false
  132. carbToUpdate.isUploadedToHealth = false
  133. carbToUpdate.isUploadedToTidepool = false
  134. guard context.hasChanges else { return }
  135. try context.save()
  136. debugPrint(
  137. "\(DebuggingIdentifiers.succeeded) Updated Carb Entry in Core Data"
  138. )
  139. }
  140. } catch {
  141. debugPrint(
  142. "\(DebuggingIdentifiers.failed) Error updating carb entry in Core Data with error: \(error.localizedDescription)"
  143. )
  144. }
  145. }
  146. }
  147. func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
  148. let taskContext = CoreDataStack.shared.newTaskContext()
  149. taskContext.name = "deleteContext"
  150. taskContext.transactionAuthor = "deleteCarbsFromServices"
  151. var carbEntry: CarbEntryStored?
  152. // Delete carbs or FPUs from Nightscout
  153. await taskContext.perform {
  154. do {
  155. carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
  156. guard let carbEntry = carbEntry else {
  157. debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
  158. return
  159. }
  160. if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
  161. // Delete Fat and Protein entries from Nightscout
  162. self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
  163. // Delete Fat and Protein entries from Apple Health
  164. let healthObjectsToDelete: [HKSampleType?] = [
  165. AppleHealthConfig.healthFatObject,
  166. AppleHealthConfig.healthProteinObject
  167. ]
  168. for sampleType in healthObjectsToDelete {
  169. if let validSampleType = sampleType {
  170. self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
  171. }
  172. }
  173. } else {
  174. // Delete carbs from Nightscout
  175. if let id = carbEntry.id, let entryDate = carbEntry.date {
  176. self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
  177. // Delete carbs from Apple Health
  178. if let sampleType = AppleHealthConfig.healthCarbObject {
  179. self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
  180. }
  181. self.provider.deleteCarbsFromTidepool(
  182. withSyncId: id,
  183. carbs: Decimal(carbEntry.carbs),
  184. at: entryDate,
  185. enteredBy: CarbsEntry.local
  186. )
  187. }
  188. }
  189. } catch {
  190. debugPrint(
  191. "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
  192. )
  193. }
  194. }
  195. }
  196. // Insulin deletion from history
  197. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  198. func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  199. Task {
  200. await invokeInsulinDeletion(treatmentObjectID)
  201. await MainActor.run {
  202. insulinEntryDeleted = true
  203. waitForSuggestion = true
  204. }
  205. }
  206. }
  207. func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
  208. do {
  209. let authenticated = try await unlockmanager.unlock()
  210. guard authenticated else {
  211. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
  212. return
  213. }
  214. // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
  215. await deleteInsulinFromServices(with: treatmentObjectID)
  216. // Delete from Core Data
  217. await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
  218. // Perform a determine basal sync to update iob
  219. await apsManager.determineBasalSync()
  220. } catch {
  221. debugPrint(
  222. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
  223. )
  224. }
  225. }
  226. func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
  227. let taskContext = CoreDataStack.shared.newTaskContext()
  228. taskContext.name = "deleteContext"
  229. taskContext.transactionAuthor = "deleteInsulinFromServices"
  230. await taskContext.perform {
  231. do {
  232. guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
  233. else {
  234. debug(.default, "Could not cast the object to PumpEventStored")
  235. return
  236. }
  237. if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
  238. let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
  239. {
  240. self.provider.deleteInsulinFromNightscout(withID: id)
  241. self.provider.deleteInsulinFromHealth(withSyncID: id)
  242. self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
  243. }
  244. taskContext.delete(treatmentToDelete)
  245. try taskContext.save()
  246. debug(.default, "Successfully deleted the treatment object.")
  247. } catch {
  248. debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
  249. }
  250. }
  251. }
  252. // Function to get the original zero-carb non-FPU entry
  253. func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
  254. let context = CoreDataStack.shared.newTaskContext()
  255. context.name = "fpuContext"
  256. return await context.perform {
  257. do {
  258. // Get the fpuID from the selected entry
  259. guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
  260. let fpuID = selectedEntry.fpuID
  261. else { return nil }
  262. // Fetch the original zero-carb entry (non-FPU) with the same fpuID
  263. let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
  264. let request = CarbEntryStored.fetchRequest()
  265. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  266. NSPredicate(format: "date >= %@", last24Hours as NSDate),
  267. NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
  268. NSPredicate(format: "isFPU == NO"),
  269. NSPredicate(format: "carbs == 0")
  270. ])
  271. request.fetchLimit = 1
  272. let originalEntry = try context.fetch(request).first
  273. debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
  274. return originalEntry?.objectID
  275. } catch let error as NSError {
  276. debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
  277. return nil
  278. }
  279. }
  280. }
  281. func updateFPUEntry(_ treatmentObjectID: NSManagedObjectID, newFat: Decimal, newProtein: Decimal, newNote: String) {
  282. Task {
  283. // Get the original entry's actualDate before deletion
  284. let context = CoreDataStack.shared.newTaskContext()
  285. let originalDate = await context.perform {
  286. do {
  287. guard let entry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored
  288. else { return Date() }
  289. return entry.date ?? Date()
  290. } catch {
  291. return Date()
  292. }
  293. }
  294. // Delete old FPU from Core Data and Remote Services and await this
  295. await deleteCarbs(treatmentObjectID)
  296. // Create new FPU entry with updated values
  297. let newEntry = CarbsEntry(
  298. id: UUID().uuidString,
  299. createdAt: Date(),
  300. actualDate: originalDate, // Use the original entry's date
  301. carbs: Decimal(0),
  302. fat: newFat,
  303. protein: newProtein,
  304. note: newNote,
  305. enteredBy: CarbsEntry.local,
  306. isFPU: true,
  307. fpuID: UUID().uuidString
  308. )
  309. // Store new entry which will create new FPU entries
  310. await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
  311. // Upload updated entries to services in parallel
  312. async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
  313. async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
  314. async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
  315. // Wait for all uploads to complete
  316. _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
  317. }
  318. }
  319. }
  320. }
  321. extension DataTable.StateModel: DeterminationObserver, SettingsObserver {
  322. func determinationDidUpdate(_: Determination) {
  323. DispatchQueue.main.async {
  324. self.waitForSuggestion = false
  325. }
  326. }
  327. func settingsDidChange(_: FreeAPSSettings) {
  328. units = settingsManager.settings.units
  329. }
  330. }