DataTableStateModel.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. 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 from Core Data
  98. await carbsStorage.deleteCarbs(treatmentObjectID)
  99. // Perform a determine basal sync to update cob
  100. await apsManager.determineBasalSync()
  101. }
  102. func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
  103. let taskContext = CoreDataStack.shared.newTaskContext()
  104. taskContext.name = "deleteContext"
  105. taskContext.transactionAuthor = "deleteCarbsFromServices"
  106. var carbEntry: CarbEntryStored?
  107. // Delete carbs or FPUs from Nightscout
  108. await taskContext.perform {
  109. do {
  110. carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
  111. guard let carbEntry = carbEntry else {
  112. debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
  113. return
  114. }
  115. if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
  116. // Delete Fat and Protein entries from Nightscout
  117. self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
  118. // Delete Fat and Protein entries from Apple Health
  119. let healthObjectsToDelete: [HKSampleType?] = [
  120. AppleHealthConfig.healthFatObject,
  121. AppleHealthConfig.healthProteinObject
  122. ]
  123. for sampleType in healthObjectsToDelete {
  124. if let validSampleType = sampleType {
  125. self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
  126. }
  127. }
  128. } else {
  129. // Delete carbs from Nightscout
  130. if let id = carbEntry.id, let entryDate = carbEntry.date {
  131. self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
  132. // Delete carbs from Apple Health
  133. if let sampleType = AppleHealthConfig.healthCarbObject {
  134. self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
  135. }
  136. self.provider.deleteCarbsFromTidepool(
  137. withSyncId: id,
  138. carbs: Decimal(carbEntry.carbs),
  139. at: entryDate,
  140. enteredBy: CarbsEntry.manual
  141. )
  142. }
  143. }
  144. } catch {
  145. debugPrint(
  146. "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
  147. )
  148. }
  149. }
  150. }
  151. // Insulin deletion from history
  152. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  153. func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  154. Task {
  155. await invokeInsulinDeletion(treatmentObjectID)
  156. await MainActor.run {
  157. insulinEntryDeleted = true
  158. waitForSuggestion = true
  159. }
  160. }
  161. }
  162. func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
  163. do {
  164. let authenticated = try await unlockmanager.unlock()
  165. guard authenticated else {
  166. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
  167. return
  168. }
  169. // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
  170. await deleteInsulinFromServices(with: treatmentObjectID)
  171. // Delete from Core Data
  172. await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
  173. // Perform a determine basal sync to update iob
  174. await apsManager.determineBasalSync()
  175. } catch {
  176. debugPrint(
  177. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
  178. )
  179. }
  180. }
  181. func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
  182. let taskContext = CoreDataStack.shared.newTaskContext()
  183. taskContext.name = "deleteContext"
  184. taskContext.transactionAuthor = "deleteInsulinFromServices"
  185. await taskContext.perform {
  186. do {
  187. guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
  188. else {
  189. debug(.default, "Could not cast the object to PumpEventStored")
  190. return
  191. }
  192. if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
  193. let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
  194. {
  195. self.provider.deleteInsulinFromNightscout(withID: id)
  196. self.provider.deleteInsulinFromHealth(withSyncID: id)
  197. self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
  198. }
  199. taskContext.delete(treatmentToDelete)
  200. try taskContext.save()
  201. debug(.default, "Successfully deleted the treatment object.")
  202. } catch {
  203. debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
  204. }
  205. }
  206. }
  207. }
  208. }
  209. extension DataTable.StateModel: DeterminationObserver, SettingsObserver {
  210. func determinationDidUpdate(_: Determination) {
  211. DispatchQueue.main.async {
  212. self.waitForSuggestion = false
  213. }
  214. }
  215. func settingsDidChange(_: FreeAPSSettings) {
  216. units = settingsManager.settings.units
  217. maxBolus = provider.pumpSettings().maxBolus
  218. }
  219. }