DataTableStateModel.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import CoreData
  2. import SwiftUI
  3. extension DataTable {
  4. final class StateModel: BaseStateModel<Provider> {
  5. @Injected() var broadcaster: Broadcaster!
  6. @Injected() var apsManager: APSManager!
  7. @Injected() var unlockmanager: UnlockManager!
  8. @Injected() private var storage: FileStorage!
  9. @Injected() var pumpHistoryStorage: PumpHistoryStorage!
  10. <<<<<<< HEAD
  11. @Injected() var healthKitManager: HealthKitManager!
  12. =======
  13. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  14. let coredataContext = CoreDataStack.shared.newTaskContext()
  15. @Published var mode: Mode = .treatments
  16. @Published var treatments: [Treatment] = []
  17. @Published var glucose: [Glucose] = []
  18. <<<<<<< HEAD
  19. @Published var meals: [Treatment] = []
  20. @Published var manualGlucose: Decimal = 0
  21. @Published var maxBolus: Decimal = 0
  22. @Published var waitForSuggestion: Bool = false
  23. =======
  24. @Published var manualGlucose: Decimal = 0
  25. @Published var maxBolus: Decimal = 0
  26. @Published var externalInsulinAmount: Decimal = 0
  27. @Published var externalInsulinDate = Date()
  28. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  29. @Published var insulinEntryDeleted: Bool = false
  30. @Published var carbEntryDeleted: Bool = false
  31. var units: GlucoseUnits = .mgdL
  32. override func subscribe() {
  33. units = settingsManager.settings.units
  34. maxBolus = provider.pumpSettings().maxBolus
  35. <<<<<<< HEAD
  36. broadcaster.register(DeterminationObserver.self, observer: self)
  37. =======
  38. setupTreatments()
  39. setupGlucose()
  40. broadcaster.register(SettingsObserver.self, observer: self)
  41. broadcaster.register(PumpHistoryObserver.self, observer: self)
  42. broadcaster.register(TempTargetsObserver.self, observer: self)
  43. broadcaster.register(CarbsObserver.self, observer: self)
  44. broadcaster.register(GlucoseObserver.self, observer: self)
  45. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  46. }
  47. // Carb and FPU deletion from history
  48. /// marked as MainActor to be able to publish changes from the background
  49. /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  50. @MainActor func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  51. Task {
  52. await deleteGlucose(treatmentObjectID)
  53. }
  54. }
  55. <<<<<<< HEAD
  56. func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
  57. let taskContext = CoreDataStack.shared.newTaskContext()
  58. taskContext.name = "deleteContext"
  59. taskContext.transactionAuthor = "deleteGlucose"
  60. await taskContext.perform {
  61. do {
  62. let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
  63. guard let glucoseToDelete = result else {
  64. debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
  65. return
  66. }
  67. // Delete Manual Glucose from Nightscout
  68. if glucoseToDelete.isManual == true {
  69. if let id = glucoseToDelete.id?.uuidString {
  70. self.provider.deleteManualGlucose(withID: id)
  71. =======
  72. let carbs = self.provider.carbs()
  73. .filter { !($0.isFPU ?? false) }
  74. .map {
  75. if let id = $0.id {
  76. return Treatment(
  77. units: units,
  78. type: .carbs,
  79. date: $0.createdAt,
  80. amount: $0.carbs,
  81. id: id,
  82. note: $0.note
  83. )
  84. } else {
  85. return Treatment(
  86. units: units,
  87. type: .carbs,
  88. date: $0.createdAt,
  89. amount: $0.carbs,
  90. note: $0.note
  91. )
  92. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  93. }
  94. }
  95. taskContext.delete(glucoseToDelete)
  96. <<<<<<< HEAD
  97. guard taskContext.hasChanges else { return }
  98. try taskContext.save()
  99. debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
  100. } catch {
  101. debugPrint(
  102. "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
  103. =======
  104. let boluses = self.provider.pumpHistory()
  105. .filter { $0.type == .bolus }
  106. .map {
  107. Treatment(
  108. units: units,
  109. type: .bolus,
  110. date: $0.timestamp,
  111. amount: $0.amount,
  112. idPumpEvent: $0.id,
  113. isSMB: $0.isSMB,
  114. isExternal: $0.isExternalInsulin
  115. )
  116. }
  117. let tempBasals = self.provider.pumpHistory()
  118. .filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
  119. .chunks(ofCount: 2)
  120. .compactMap { chunk -> Treatment? in
  121. let chunk = Array(chunk)
  122. guard chunk.count == 2, chunk[0].type == .tempBasal,
  123. chunk[1].type == .tempBasalDuration else { return nil }
  124. return Treatment(
  125. units: units,
  126. type: .tempBasal,
  127. date: chunk[0].timestamp,
  128. amount: chunk[0].rate ?? 0,
  129. secondAmount: nil,
  130. duration: Decimal(chunk[1].durationMin ?? 0)
  131. )
  132. }
  133. let tempTargets = self.provider.tempTargets()
  134. .map {
  135. Treatment(
  136. units: units,
  137. type: .tempTarget,
  138. date: $0.createdAt,
  139. amount: $0.targetBottom ?? 0,
  140. secondAmount: $0.targetTop,
  141. duration: $0.duration
  142. )
  143. }
  144. let suspend = self.provider.pumpHistory()
  145. .filter { $0.type == .pumpSuspend }
  146. .map {
  147. Treatment(units: units, type: .suspend, date: $0.timestamp)
  148. }
  149. let resume = self.provider.pumpHistory()
  150. .filter { $0.type == .pumpResume }
  151. .map {
  152. Treatment(units: units, type: .resume, date: $0.timestamp)
  153. }
  154. DispatchQueue.main.async {
  155. self.treatments = [carbs, boluses, tempBasals, tempTargets, suspend, resume, fpus]
  156. .flatMap { $0 }
  157. .sorted { $0.date > $1.date }
  158. }
  159. }
  160. }
  161. func setupGlucose() {
  162. DispatchQueue.main.async {
  163. self.glucose = self.provider.glucose().map(Glucose.init)
  164. }
  165. }
  166. func deleteCarbs(_ treatment: Treatment) {
  167. provider.deleteCarbs(treatment)
  168. }
  169. func deleteInsulin(_ treatment: Treatment) {
  170. unlockmanager.unlock()
  171. .sink { _ in } receiveValue: { [weak self] _ in
  172. guard let self = self else { return }
  173. self.provider.deleteInsulin(treatment)
  174. }
  175. .store(in: &lifetime)
  176. }
  177. func deleteGlucose(_ glucose: Glucose) {
  178. let id = glucose.id
  179. provider.deleteGlucose(id: id)
  180. let fetchRequest: NSFetchRequest<NSFetchRequestResult>
  181. fetchRequest = NSFetchRequest(entityName: "Readings")
  182. fetchRequest.predicate = NSPredicate(format: "id == %@", id)
  183. let deleteRequest = NSBatchDeleteRequest(
  184. fetchRequest: fetchRequest
  185. )
  186. deleteRequest.resultType = .resultTypeObjectIDs
  187. do {
  188. let deleteResult = try coredataContext.execute(deleteRequest) as? NSBatchDeleteResult
  189. if let objectIDs = deleteResult?.result as? [NSManagedObjectID] {
  190. NSManagedObjectContext.mergeChanges(
  191. fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs],
  192. into: [coredataContext]
  193. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  194. )
  195. }
  196. }
  197. }
  198. // Carb and FPU deletion from history
  199. /// marked as MainActor to be able to publish changes from the background
  200. /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  201. @MainActor func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  202. Task {
  203. await deleteCarbs(treatmentObjectID)
  204. carbEntryDeleted = true
  205. waitForSuggestion = true
  206. }
  207. }
  208. func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
  209. let taskContext = CoreDataStack.shared.newTaskContext()
  210. taskContext.name = "deleteContext"
  211. taskContext.transactionAuthor = "deleteCarbs"
  212. var carbEntry: CarbEntryStored?
  213. await taskContext.perform {
  214. do {
  215. carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
  216. guard let carbEntry = carbEntry else {
  217. debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
  218. return
  219. }
  220. if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
  221. // Delete FPUs from Nightscout
  222. self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
  223. // fetch request for all carb entries with the same id
  224. let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
  225. fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
  226. // NSBatchDeleteRequest
  227. let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
  228. deleteRequest.resultType = .resultTypeCount
  229. // execute the batch delete request
  230. let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
  231. debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
  232. Foundation.NotificationCenter.default.post(name: .didPerformBatchDelete, object: nil)
  233. } else {
  234. // Delete carbs from Nightscout
  235. if let id = carbEntry.id?.uuidString {
  236. self.provider.deleteCarbsFromNightscout(withID: id)
  237. }
  238. // Now delete carbs also from the Database
  239. taskContext.delete(carbEntry)
  240. guard taskContext.hasChanges else { return }
  241. try taskContext.save()
  242. debugPrint(
  243. "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
  244. )
  245. }
  246. } catch {
  247. debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
  248. }
  249. }
  250. // Perform a determine basal sync to update cob
  251. await apsManager.determineBasalSync()
  252. }
  253. // Insulin deletion from history
  254. /// marked as MainActor to be able to publish changes from the background
  255. /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  256. @MainActor func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  257. Task {
  258. await deleteInsulin(treatmentObjectID)
  259. insulinEntryDeleted = true
  260. waitForSuggestion = true
  261. }
  262. }
  263. func deleteInsulin(_ treatmentObjectID: NSManagedObjectID) async {
  264. do {
  265. let authenticated = try await unlockmanager.unlock()
  266. guard authenticated else {
  267. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
  268. return
  269. }
  270. async let deleteNSManagedObjectTask: () = CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
  271. async let deleteInsulinFromNightScoutTask: () = provider.deleteInsulin(with: treatmentObjectID)
  272. async let determineBasalTask: () = apsManager.determineBasalSync()
  273. await deleteNSManagedObjectTask
  274. await deleteInsulinFromNightScoutTask
  275. await determineBasalTask
  276. } catch {
  277. debugPrint(
  278. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
  279. )
  280. }
  281. }
  282. <<<<<<< HEAD
  283. func addManualGlucose() {
  284. let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
  285. let glucoseAsInt = Int(glucose)
  286. =======
  287. func logManualGlucose() {
  288. let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
  289. let now = Date()
  290. let id = UUID().uuidString
  291. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  292. // save to core data
  293. coredataContext.perform {
  294. let newItem = GlucoseStored(context: self.coredataContext)
  295. newItem.id = UUID()
  296. newItem.date = Date()
  297. newItem.glucose = Int16(glucoseAsInt)
  298. newItem.isManual = true
  299. newItem.isUploadedToNS = false
  300. do {
  301. guard self.coredataContext.hasChanges else { return }
  302. try self.coredataContext.save()
  303. } catch {
  304. print(error.localizedDescription)
  305. }
  306. }
  307. }
  308. func logExternalInsulin() {
  309. guard externalInsulinAmount > 0 else {
  310. showModal(for: nil)
  311. return
  312. }
  313. externalInsulinAmount = min(externalInsulinAmount, maxBolus * 3) // Allow for 3 * Max Bolus for external insulin
  314. unlockmanager.unlock()
  315. .sink { _ in } receiveValue: { [weak self] _ in
  316. guard let self = self else { return }
  317. pumpHistoryStorage.storeEvents(
  318. [
  319. PumpHistoryEvent(
  320. id: UUID().uuidString,
  321. type: .bolus,
  322. timestamp: externalInsulinDate,
  323. amount: externalInsulinAmount,
  324. duration: nil,
  325. durationMin: nil,
  326. rate: nil,
  327. temp: nil,
  328. carbInput: nil,
  329. isExternalInsulin: true
  330. )
  331. ]
  332. )
  333. debug(.default, "External insulin saved to pumphistory.json")
  334. // Reset amount to 0 for next entry
  335. externalInsulinAmount = 0
  336. }
  337. .store(in: &lifetime)
  338. }
  339. }
  340. }
  341. extension DataTable.StateModel: DeterminationObserver {
  342. func determinationDidUpdate(_: Determination) {
  343. DispatchQueue.main.async {
  344. self.waitForSuggestion = false
  345. }
  346. }
  347. }