GlucoseStorage.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import AVFAudio
  2. import CoreData
  3. import Foundation
  4. import SwiftDate
  5. import SwiftUI
  6. import Swinject
  7. protocol GlucoseStorage {
  8. func storeGlucose(_ glucose: [BloodGlucose])
  9. func syncDate() -> Date
  10. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
  11. func lastGlucoseDate() -> Date
  12. func isGlucoseFresh() -> Bool
  13. func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
  14. func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
  15. func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment]
  16. var alarm: GlucoseAlarm? { get }
  17. func fetchGlucose() -> [GlucoseStored]
  18. }
  19. final class BaseGlucoseStorage: GlucoseStorage, Injectable {
  20. private let processQueue = DispatchQueue(label: "BaseGlucoseStorage.processQueue")
  21. @Injected() private var storage: FileStorage!
  22. @Injected() private var broadcaster: Broadcaster!
  23. @Injected() private var settingsManager: SettingsManager!
  24. let coredataContext = CoreDataStack.shared.newTaskContext()
  25. private enum Config {
  26. static let filterTime: TimeInterval = 3.5 * 60
  27. }
  28. init(resolver: Resolver) {
  29. injectServices(resolver)
  30. }
  31. private var glucoseFormatter: NumberFormatter {
  32. let formatter = NumberFormatter()
  33. formatter.numberStyle = .decimal
  34. formatter.maximumFractionDigits = 0
  35. if settingsManager.settings.units == .mmolL {
  36. formatter.maximumFractionDigits = 1
  37. }
  38. formatter.decimalSeparator = "."
  39. return formatter
  40. }
  41. func storeGlucose(_ glucose: [BloodGlucose]) {
  42. processQueue.sync {
  43. debug(.deviceManager, "Start storage of glucose data")
  44. self.coredataContext.perform {
  45. let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
  46. let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
  47. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  48. NSPredicate(format: "date IN %@", datesToCheck),
  49. NSPredicate.predicateForOneDayAgo
  50. ])
  51. fetchRequest.propertiesToFetch = ["date"]
  52. fetchRequest.resultType = .dictionaryResultType
  53. var existingDates = Set<Date>()
  54. do {
  55. let results = try self.coredataContext.fetch(fetchRequest) as? [NSDictionary]
  56. existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
  57. } catch {
  58. debugPrint("Failed to fetch existing glucose dates: \(error)")
  59. }
  60. var filteredGlucose = glucose.filter { !existingDates.contains($0.dateString) }
  61. // prepare batch insert
  62. let batchInsert = NSBatchInsertRequest(
  63. entity: GlucoseStored.entity(),
  64. managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
  65. guard let glucoseEntry = managedObject as? GlucoseStored, !filteredGlucose.isEmpty else {
  66. return true // Stop if there are no more items
  67. }
  68. let entry = filteredGlucose.removeFirst()
  69. glucoseEntry.id = UUID()
  70. glucoseEntry.glucose = Int16(entry.glucose ?? 0)
  71. glucoseEntry.date = entry.dateString
  72. glucoseEntry.direction = entry.direction?.symbol
  73. return false // Continue processing
  74. }
  75. )
  76. // process batch insert
  77. do {
  78. try self.coredataContext.execute(batchInsert)
  79. debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
  80. // Send notification for triggering a fetch in Home State Model to update the Glucose Array
  81. /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
  82. /// But I do not want to fetch on the Main Thread using the @FetchRequest property, I also can not use the FetchedResultsController because of the architecture of the State Model (it must inherit from BaseStateModel and therefore can not inherit from NSObject as well) and because of the fact that I am using a batch insert here there are no notifications sent from the managedObjectContext because changes are directly stored in the persistent container
  83. Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
  84. } catch {
  85. debugPrint(
  86. "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
  87. )
  88. }
  89. debug(.deviceManager, "start storage cgmState")
  90. self.storage.transaction { storage in
  91. let file = OpenAPS.Monitor.cgmState
  92. var treatments = storage.retrieve(file, as: [NigtscoutTreatment].self) ?? []
  93. var updated = false
  94. for x in glucose {
  95. debug(.deviceManager, "storeGlucose \(x)")
  96. guard let sessionStartDate = x.sessionStartDate else {
  97. continue
  98. }
  99. if let lastTreatment = treatments.last,
  100. let createdAt = lastTreatment.createdAt,
  101. // When a new Dexcom sensor is started, it produces multiple consecutive
  102. // startDates. Disambiguate them by only allowing a session start per minute.
  103. abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
  104. {
  105. continue
  106. }
  107. var notes = ""
  108. if let t = x.transmitterID {
  109. notes = t
  110. }
  111. if let a = x.activationDate {
  112. notes = "\(notes) activated on \(a)"
  113. }
  114. let treatment = NigtscoutTreatment(
  115. duration: nil,
  116. rawDuration: nil,
  117. rawRate: nil,
  118. absolute: nil,
  119. rate: nil,
  120. eventType: .nsSensorChange,
  121. createdAt: sessionStartDate,
  122. enteredBy: NigtscoutTreatment.local,
  123. bolus: nil,
  124. insulin: nil,
  125. notes: notes,
  126. carbs: nil,
  127. fat: nil,
  128. protein: nil,
  129. targetTop: nil,
  130. targetBottom: nil
  131. )
  132. debug(.deviceManager, "CGM sensor change \(treatment)")
  133. treatments.append(treatment)
  134. updated = true
  135. }
  136. if updated {
  137. // We have to keep quite a bit of history as sensors start only every 10 days.
  138. storage.save(
  139. treatments.filter
  140. { $0.createdAt != nil && $0.createdAt!.addingTimeInterval(30.days.timeInterval) > Date() },
  141. as: file
  142. )
  143. }
  144. }
  145. }
  146. }
  147. }
  148. func syncDate() -> Date {
  149. let fr = GlucoseStored.fetchRequest()
  150. fr.predicate = NSPredicate.predicateForOneDayAgo
  151. fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
  152. fr.fetchLimit = 1
  153. var date: Date?
  154. coredataContext.performAndWait {
  155. do {
  156. let results = try self.coredataContext.fetch(fr)
  157. date = results.first?.date
  158. } catch let error as NSError {
  159. print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
  160. }
  161. }
  162. return date ?? .distantPast
  163. }
  164. func lastGlucoseDate() -> Date {
  165. let fr = GlucoseStored.fetchRequest()
  166. fr.predicate = NSPredicate.predicateForOneDayAgo
  167. fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
  168. fr.fetchLimit = 1
  169. var date: Date?
  170. coredataContext.performAndWait {
  171. do {
  172. let results = try self.coredataContext.fetch(fr)
  173. date = results.first?.date
  174. } catch let error as NSError {
  175. print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
  176. }
  177. }
  178. return date ?? .distantPast
  179. }
  180. func isGlucoseFresh() -> Bool {
  181. Date().timeIntervalSince(lastGlucoseDate()) <= Config.filterTime
  182. }
  183. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at date: Date) -> [BloodGlucose] {
  184. var lastDate = date
  185. var filtered: [BloodGlucose] = []
  186. let sorted = glucose.sorted { $0.date < $1.date }
  187. for entry in sorted {
  188. guard entry.dateString.addingTimeInterval(-Config.filterTime) > lastDate else {
  189. continue
  190. }
  191. filtered.append(entry)
  192. lastDate = entry.dateString
  193. }
  194. return filtered
  195. }
  196. // MARK: - fetching non manual Glucose, manual Glucose and the last glucose value
  197. // TODO: -optimize this bullshit here...I would love to use the async/await pattern, but its simply not possible because you would need to change all the calls of the following functions and make them async...same shit with the NSAsynchronousFetchRequest
  198. /// its all done on a background thread and on a separate queue so hopefully its not too heavy
  199. /// also tried this but here again you need to make everything asynchronous...
  200. /// let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
  201. /// privateContext.parent = coredataContext /// merges changes to the core data context
  202. ///
  203. func fetchGlucose() -> [GlucoseStored] {
  204. let predicate = NSPredicate.predicateForOneDayAgo
  205. return CoreDataStack.shared.fetchEntities(
  206. ofType: GlucoseStored.self,
  207. onContext: coredataContext,
  208. predicate: predicate,
  209. key: "date",
  210. ascending: false,
  211. fetchLimit: 288,
  212. batchSize: 50
  213. )
  214. }
  215. func fetchManualGlucose() -> [GlucoseStored] {
  216. let predicate = NSPredicate.manualGlucose
  217. return CoreDataStack.shared.fetchEntities(
  218. ofType: GlucoseStored.self,
  219. onContext: coredataContext,
  220. predicate: predicate,
  221. key: "date",
  222. ascending: false,
  223. fetchLimit: 288,
  224. batchSize: 50
  225. )
  226. }
  227. func fetchLatestGlucose() -> GlucoseStored? {
  228. let predicate = NSPredicate.predicateFor20MinAgo
  229. return CoreDataStack.shared.fetchEntities(
  230. ofType: GlucoseStored.self,
  231. onContext: coredataContext,
  232. predicate: predicate,
  233. key: "date",
  234. ascending: false,
  235. fetchLimit: 1
  236. ).first
  237. }
  238. private func processManualGlucose() -> [BloodGlucose] {
  239. coredataContext.performAndWait {
  240. let fetchedResults = fetchManualGlucose()
  241. let glucoseArray = fetchedResults.map { result in
  242. BloodGlucose(
  243. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  244. dateString: result.date ?? Date(),
  245. unfiltered: Decimal(result.glucose),
  246. filtered: Decimal(result.glucose),
  247. noise: nil,
  248. type: ""
  249. )
  250. }
  251. return glucoseArray
  252. }
  253. }
  254. private func processGlucose() -> [BloodGlucose] {
  255. coredataContext.performAndWait {
  256. let results = self.fetchGlucose()
  257. let glucoseArray = results.map { result in
  258. BloodGlucose(
  259. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  260. dateString: result.date ?? Date(),
  261. unfiltered: Decimal(result.glucose),
  262. filtered: Decimal(result.glucose),
  263. noise: nil,
  264. type: ""
  265. )
  266. }
  267. return glucoseArray
  268. }
  269. }
  270. func nightscoutGlucoseNotUploaded() -> [BloodGlucose] {
  271. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? []
  272. let recentGlucose = processGlucose()
  273. return Array(Set(recentGlucose).subtracting(Set(uploaded)))
  274. }
  275. func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment] {
  276. let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCGMState, as: [NigtscoutTreatment].self) ?? []
  277. let recent = storage.retrieve(OpenAPS.Monitor.cgmState, as: [NigtscoutTreatment].self) ?? []
  278. return Array(Set(recent).subtracting(Set(uploaded)))
  279. }
  280. func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment] {
  281. let uploaded = (storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? [])
  282. .filter({ $0.type == GlucoseType.manual.rawValue })
  283. let recent = processManualGlucose()
  284. let filtered = Array(Set(recent).subtracting(Set(uploaded)))
  285. let manualReadings = filtered.map { item -> NigtscoutTreatment in
  286. NigtscoutTreatment(
  287. duration: nil, rawDuration: nil, rawRate: nil, absolute: nil, rate: nil, eventType: .capillaryGlucose,
  288. createdAt: item.dateString, enteredBy: "iAPS", bolus: nil, insulin: nil, notes: "iAPS User", carbs: nil,
  289. fat: nil,
  290. protein: nil, foodType: nil, targetTop: nil, targetBottom: nil, glucoseType: "Manual",
  291. glucose: settingsManager.settings
  292. .units == .mgdL ? (glucoseFormatter.string(from: Int(item.glucose ?? 100) as NSNumber) ?? "")
  293. : (glucoseFormatter.string(from: Decimal(item.glucose ?? 100).asMmolL as NSNumber) ?? ""),
  294. units: settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl"
  295. )
  296. }
  297. return manualReadings
  298. }
  299. var alarm: GlucoseAlarm? {
  300. /// glucose can not be older than 20 minutes due to the predicate in the fetch request
  301. coredataContext.performAndWait {
  302. guard let glucose = fetchLatestGlucose() else { return nil }
  303. let glucoseValue = glucose.glucose
  304. if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
  305. return .low
  306. }
  307. if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
  308. return .high
  309. }
  310. return nil
  311. }
  312. }
  313. }
  314. protocol GlucoseObserver {
  315. func glucoseDidUpdate(_ glucose: [BloodGlucose])
  316. }
  317. enum GlucoseAlarm {
  318. case high
  319. case low
  320. var displayName: String {
  321. switch self {
  322. case .high:
  323. return NSLocalizedString("LOWALERT!", comment: "LOWALERT!")
  324. case .low:
  325. return NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!")
  326. }
  327. }
  328. }