DataTableStateModel.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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 maxBolus: Decimal = 0
  22. var waitForSuggestion: Bool = false
  23. var insulinEntryDeleted: Bool = false
  24. var carbEntryDeleted: Bool = false
  25. var units: GlucoseUnits = .mgdL
  26. override func subscribe() {
  27. units = settingsManager.settings.units
  28. maxBolus = provider.pumpSettings().maxBolus
  29. broadcaster.register(DeterminationObserver.self, observer: self)
  30. }
  31. func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
  32. glucoseStorage.isGlucoseDataFresh(glucoseDate)
  33. }
  34. // Glucose deletion from history and from remote services
  35. /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  36. func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  37. Task {
  38. await deleteGlucose(treatmentObjectID)
  39. }
  40. }
  41. func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
  42. // Delete from Apple Health/Tidepool
  43. await deleteGlucoseFromServices(treatmentObjectID)
  44. // Delete from Core Data
  45. await glucoseStorage.deleteGlucose(treatmentObjectID)
  46. }
  47. func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
  48. let taskContext = CoreDataStack.shared.newTaskContext()
  49. taskContext.name = "deleteContext"
  50. taskContext.transactionAuthor = "deleteGlucoseFromServices"
  51. await taskContext.perform {
  52. do {
  53. let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
  54. guard let glucoseToDelete = result else {
  55. debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
  56. return
  57. }
  58. // Delete from Nightscout
  59. if let id = glucoseToDelete.id?.uuidString {
  60. self.provider.deleteManualGlucoseFromNightscout(withID: id)
  61. }
  62. // Delete from Apple Health
  63. if let id = glucoseToDelete.id?.uuidString {
  64. self.provider.deleteGlucoseFromHealth(withSyncID: id)
  65. }
  66. debugPrint(
  67. "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
  68. )
  69. } catch {
  70. debugPrint(
  71. "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
  72. )
  73. }
  74. }
  75. }
  76. // Carb and FPU deletion from history
  77. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  78. func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  79. Task {
  80. await deleteCarbs(treatmentObjectID)
  81. await MainActor.run {
  82. carbEntryDeleted = true
  83. waitForSuggestion = true
  84. }
  85. }
  86. }
  87. func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
  88. // Delete from Apple Health/Tidepool
  89. await deleteCarbsFromServices(treatmentObjectID)
  90. // Delete carbs from Core Data
  91. await carbsStorage.deleteCarbs(treatmentObjectID)
  92. // Perform a determine basal sync to update cob
  93. await apsManager.determineBasalSync()
  94. }
  95. func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
  96. let taskContext = CoreDataStack.shared.newTaskContext()
  97. taskContext.name = "deleteContext"
  98. taskContext.transactionAuthor = "deleteCarbsFromServices"
  99. var carbEntry: CarbEntryStored?
  100. // Delete carbs or FPUs from Nightscout
  101. await taskContext.perform {
  102. do {
  103. carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
  104. guard let carbEntry = carbEntry else {
  105. debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
  106. return
  107. }
  108. if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
  109. // Delete Fat and Protein entries from Nightscout
  110. self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
  111. // Delete Fat and Protein entries from Apple Health
  112. let healthObjectsToDelete: [HKSampleType?] = [
  113. AppleHealthConfig.healthFatObject,
  114. AppleHealthConfig.healthProteinObject
  115. ]
  116. for sampleType in healthObjectsToDelete {
  117. if let validSampleType = sampleType {
  118. self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
  119. }
  120. }
  121. } else {
  122. // Delete carbs from Nightscout
  123. if let id = carbEntry.id, let entryDate = carbEntry.date {
  124. self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
  125. // Delete carbs from Apple Health
  126. if let sampleType = AppleHealthConfig.healthCarbObject {
  127. self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
  128. }
  129. self.provider.deleteCarbsFromTidepool(
  130. withSyncId: id,
  131. carbs: Decimal(carbEntry.carbs),
  132. at: entryDate,
  133. enteredBy: CarbsEntry.manual
  134. )
  135. }
  136. }
  137. } catch {
  138. debugPrint(
  139. "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
  140. )
  141. }
  142. }
  143. }
  144. // Insulin deletion from history
  145. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  146. func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  147. Task {
  148. await invokeInsulinDeletion(treatmentObjectID)
  149. await MainActor.run {
  150. insulinEntryDeleted = true
  151. waitForSuggestion = true
  152. }
  153. }
  154. }
  155. func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
  156. do {
  157. let authenticated = try await unlockmanager.unlock()
  158. guard authenticated else {
  159. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
  160. return
  161. }
  162. // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
  163. await deleteInsulinFromServices(with: treatmentObjectID)
  164. // Delete from Core Data
  165. await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
  166. // Perform a determine basal sync to update iob
  167. await apsManager.determineBasalSync()
  168. } catch {
  169. debugPrint(
  170. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
  171. )
  172. }
  173. }
  174. func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
  175. let taskContext = CoreDataStack.shared.newTaskContext()
  176. taskContext.name = "deleteContext"
  177. taskContext.transactionAuthor = "deleteInsulinFromServices"
  178. await taskContext.perform {
  179. do {
  180. guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
  181. else {
  182. debug(.default, "Could not cast the object to PumpEventStored")
  183. return
  184. }
  185. if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
  186. let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
  187. {
  188. self.provider.deleteInsulinFromNightscout(withID: id)
  189. self.provider.deleteInsulinFromHealth(withSyncID: id)
  190. self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
  191. }
  192. taskContext.delete(treatmentToDelete)
  193. try taskContext.save()
  194. debug(.default, "Successfully deleted the treatment object.")
  195. } catch {
  196. debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
  197. }
  198. }
  199. }
  200. func addManualGlucose() {
  201. let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
  202. let glucoseAsInt = Int(glucose)
  203. // save to core data
  204. coredataContext.perform {
  205. let newItem = GlucoseStored(context: self.coredataContext)
  206. newItem.id = UUID()
  207. newItem.date = Date()
  208. newItem.glucose = Int16(glucoseAsInt)
  209. newItem.isManual = true
  210. newItem.isUploadedToNS = false
  211. newItem.isUploadedToHealth = false
  212. newItem.isUploadedToTidepool = false
  213. do {
  214. guard self.coredataContext.hasChanges else { return }
  215. try self.coredataContext.save()
  216. } catch {
  217. print(error.localizedDescription)
  218. }
  219. }
  220. }
  221. }
  222. }
  223. extension DataTable.StateModel: DeterminationObserver {
  224. func determinationDidUpdate(_: Determination) {
  225. DispatchQueue.main.async {
  226. self.waitForSuggestion = false
  227. }
  228. }
  229. }