GlucoseStorage.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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 isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
  10. func syncDate() -> Date
  11. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
  12. func lastGlucoseDate() -> Date
  13. func isGlucoseFresh() -> Bool
  14. func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
  15. func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  16. func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
  17. func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
  18. func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
  19. var alarm: GlucoseAlarm? { get }
  20. }
  21. final class BaseGlucoseStorage: GlucoseStorage, Injectable {
  22. private let processQueue = DispatchQueue(label: "BaseGlucoseStorage.processQueue")
  23. @Injected() private var storage: FileStorage!
  24. @Injected() private var broadcaster: Broadcaster!
  25. @Injected() private var settingsManager: SettingsManager!
  26. let coredataContext = CoreDataStack.shared.newTaskContext()
  27. private enum Config {
  28. static let filterTime: TimeInterval = 3.5 * 60
  29. }
  30. init(resolver: Resolver) {
  31. injectServices(resolver)
  32. }
  33. private var glucoseFormatter: NumberFormatter {
  34. let formatter = NumberFormatter()
  35. formatter.numberStyle = .decimal
  36. formatter.maximumFractionDigits = 0
  37. if settingsManager.settings.units == .mmolL {
  38. formatter.maximumFractionDigits = 1
  39. }
  40. formatter.decimalSeparator = "."
  41. return formatter
  42. }
  43. func storeGlucose(_ glucose: [BloodGlucose]) {
  44. processQueue.sync {
  45. self.coredataContext.perform {
  46. let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
  47. let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
  48. fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  49. NSPredicate(format: "date IN %@", datesToCheck),
  50. NSPredicate.predicateForOneDayAgo
  51. ])
  52. fetchRequest.propertiesToFetch = ["date"]
  53. fetchRequest.resultType = .dictionaryResultType
  54. var existingDates = Set<Date>()
  55. do {
  56. let results = try self.coredataContext.fetch(fetchRequest) as? [NSDictionary]
  57. existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
  58. } catch {
  59. debugPrint("Failed to fetch existing glucose dates: \(error)")
  60. }
  61. var filteredGlucose = glucose.filter { !existingDates.contains($0.dateString) }
  62. // prepare batch insert
  63. let batchInsert = NSBatchInsertRequest(
  64. entity: GlucoseStored.entity(),
  65. managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
  66. guard let glucoseEntry = managedObject as? GlucoseStored, !filteredGlucose.isEmpty else {
  67. return true // Stop if there are no more items
  68. }
  69. let entry = filteredGlucose.removeFirst()
  70. glucoseEntry.id = UUID()
  71. glucoseEntry.glucose = Int16(entry.glucose ?? 0)
  72. glucoseEntry.date = entry.dateString
  73. glucoseEntry.direction = entry.direction?.rawValue
  74. glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
  75. glucoseEntry.isUploadedToHealth = false /// the value is not uploaded to Health (yet)
  76. return false // Continue processing
  77. }
  78. )
  79. // process batch insert
  80. do {
  81. try self.coredataContext.execute(batchInsert)
  82. // debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
  83. // Send notification for triggering a fetch in Home State Model to update the Glucose Array
  84. /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
  85. /// 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
  86. Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
  87. } catch {
  88. debugPrint(
  89. "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
  90. )
  91. }
  92. debug(.deviceManager, "start storage cgmState")
  93. self.storage.transaction { storage in
  94. let file = OpenAPS.Monitor.cgmState
  95. var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
  96. var updated = false
  97. for x in glucose {
  98. debug(.deviceManager, "storeGlucose \(x)")
  99. guard let sessionStartDate = x.sessionStartDate else {
  100. continue
  101. }
  102. if let lastTreatment = treatments.last,
  103. let createdAt = lastTreatment.createdAt,
  104. // When a new Dexcom sensor is started, it produces multiple consecutive
  105. // startDates. Disambiguate them by only allowing a session start per minute.
  106. abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
  107. {
  108. continue
  109. }
  110. var notes = ""
  111. if let t = x.transmitterID {
  112. notes = t
  113. }
  114. if let a = x.activationDate {
  115. notes = "\(notes) activated on \(a)"
  116. }
  117. let treatment = NightscoutTreatment(
  118. duration: nil,
  119. rawDuration: nil,
  120. rawRate: nil,
  121. absolute: nil,
  122. rate: nil,
  123. eventType: .nsSensorChange,
  124. createdAt: sessionStartDate,
  125. enteredBy: NightscoutTreatment.local,
  126. bolus: nil,
  127. insulin: nil,
  128. notes: notes,
  129. carbs: nil,
  130. fat: nil,
  131. protein: nil,
  132. targetTop: nil,
  133. targetBottom: nil
  134. )
  135. debug(.deviceManager, "CGM sensor change \(treatment)")
  136. treatments.append(treatment)
  137. updated = true
  138. }
  139. if updated {
  140. // We have to keep quite a bit of history as sensors start only every 10 days.
  141. storage.save(
  142. treatments.filter
  143. { $0.createdAt != nil && $0.createdAt!.addingTimeInterval(30.days.timeInterval) > Date() },
  144. as: file
  145. )
  146. }
  147. }
  148. }
  149. }
  150. }
  151. func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
  152. guard let glucoseDate = glucoseDate else { return false }
  153. return glucoseDate > Date().addingTimeInterval(-6 * 60)
  154. }
  155. func syncDate() -> Date {
  156. let fr = GlucoseStored.fetchRequest()
  157. fr.predicate = NSPredicate.predicateForOneDayAgo
  158. fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
  159. fr.fetchLimit = 1
  160. var date: Date?
  161. coredataContext.performAndWait {
  162. do {
  163. let results = try self.coredataContext.fetch(fr)
  164. date = results.first?.date
  165. } catch let error as NSError {
  166. print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
  167. }
  168. }
  169. return date ?? .distantPast
  170. }
  171. func lastGlucoseDate() -> Date {
  172. let fr = GlucoseStored.fetchRequest()
  173. fr.predicate = NSPredicate.predicateForOneDayAgo
  174. fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
  175. fr.fetchLimit = 1
  176. var date: Date?
  177. coredataContext.performAndWait {
  178. do {
  179. let results = try self.coredataContext.fetch(fr)
  180. date = results.first?.date
  181. } catch let error as NSError {
  182. print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
  183. }
  184. }
  185. return date ?? .distantPast
  186. }
  187. func isGlucoseFresh() -> Bool {
  188. Date().timeIntervalSince(lastGlucoseDate()) <= Config.filterTime
  189. }
  190. func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at date: Date) -> [BloodGlucose] {
  191. var lastDate = date
  192. var filtered: [BloodGlucose] = []
  193. let sorted = glucose.sorted { $0.date < $1.date }
  194. for entry in sorted {
  195. guard entry.dateString.addingTimeInterval(-Config.filterTime) > lastDate else {
  196. continue
  197. }
  198. filtered.append(entry)
  199. lastDate = entry.dateString
  200. }
  201. return filtered
  202. }
  203. func fetchLatestGlucose() -> GlucoseStored? {
  204. let predicate = NSPredicate.predicateFor20MinAgo
  205. return CoreDataStack.shared.fetchEntities(
  206. ofType: GlucoseStored.self,
  207. onContext: coredataContext,
  208. predicate: predicate,
  209. key: "date",
  210. ascending: false,
  211. fetchLimit: 1
  212. ).first
  213. }
  214. // Fetch glucose that is not uploaded to Nightscout yet
  215. /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
  216. func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
  217. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  218. ofType: GlucoseStored.self,
  219. onContext: coredataContext,
  220. predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
  221. key: "date",
  222. ascending: false,
  223. fetchLimit: 288
  224. )
  225. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  226. return await coredataContext.perform {
  227. return fetchedResults.map { result in
  228. BloodGlucose(
  229. _id: result.id?.uuidString ?? UUID().uuidString,
  230. sgv: Int(result.glucose),
  231. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  232. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  233. dateString: result.date ?? Date(),
  234. unfiltered: Decimal(result.glucose),
  235. filtered: Decimal(result.glucose),
  236. noise: nil,
  237. glucose: Int(result.glucose)
  238. )
  239. }
  240. }
  241. }
  242. // Fetch manual glucose that is not uploaded to Nightscout yet
  243. /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
  244. func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  245. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  246. ofType: GlucoseStored.self,
  247. onContext: coredataContext,
  248. predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
  249. key: "date",
  250. ascending: false,
  251. fetchLimit: 288
  252. )
  253. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  254. return await coredataContext.perform {
  255. return fetchedResults.map { result in
  256. NightscoutTreatment(
  257. duration: nil,
  258. rawDuration: nil,
  259. rawRate: nil,
  260. absolute: nil,
  261. rate: nil,
  262. eventType: .capillaryGlucose,
  263. createdAt: result.date,
  264. enteredBy: "Trio",
  265. bolus: nil,
  266. insulin: nil,
  267. notes: "Trio User",
  268. carbs: nil,
  269. fat: nil,
  270. protein: nil,
  271. foodType: nil,
  272. targetTop: nil,
  273. targetBottom: nil,
  274. glucoseType: "Manual",
  275. glucose: self.settingsManager.settings
  276. .units == .mgdL ? (self.glucoseFormatter.string(from: Int(result.glucose) as NSNumber) ?? "")
  277. : (self.glucoseFormatter.string(from: Decimal(result.glucose).asMmolL as NSNumber) ?? ""),
  278. units: self.settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl",
  279. id: result.id?.uuidString
  280. )
  281. }
  282. }
  283. }
  284. func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
  285. async let alreadyUploaded: [NightscoutTreatment] = storage
  286. .retrieveAsync(OpenAPS.Nightscout.uploadedCGMState, as: [NightscoutTreatment].self) ?? []
  287. async let allValues: [NightscoutTreatment] = storage
  288. .retrieveAsync(OpenAPS.Monitor.cgmState, as: [NightscoutTreatment].self) ?? []
  289. let (alreadyUploadedValues, allValuesSet) = await (alreadyUploaded, allValues)
  290. return Array(Set(allValuesSet).subtracting(Set(alreadyUploadedValues)))
  291. }
  292. // Fetch glucose that is not uploaded to Nightscout yet
  293. /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
  294. func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
  295. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  296. ofType: GlucoseStored.self,
  297. onContext: coredataContext,
  298. predicate: NSPredicate.glucoseNotYetUploadedToHealth,
  299. key: "date",
  300. ascending: false,
  301. fetchLimit: 288
  302. )
  303. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  304. return await coredataContext.perform {
  305. return fetchedResults.map { result in
  306. BloodGlucose(
  307. _id: result.id?.uuidString ?? UUID().uuidString,
  308. sgv: Int(result.glucose),
  309. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  310. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  311. dateString: result.date ?? Date(),
  312. unfiltered: Decimal(result.glucose),
  313. filtered: Decimal(result.glucose),
  314. noise: nil,
  315. glucose: Int(result.glucose)
  316. )
  317. }
  318. }
  319. }
  320. // Fetch manual glucose that is not uploaded to Nightscout yet
  321. /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
  322. func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
  323. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  324. ofType: GlucoseStored.self,
  325. onContext: coredataContext,
  326. predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
  327. key: "date",
  328. ascending: false,
  329. fetchLimit: 288
  330. )
  331. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  332. return await coredataContext.perform {
  333. return fetchedResults.map { result in
  334. BloodGlucose(
  335. _id: result.id?.uuidString ?? UUID().uuidString,
  336. sgv: Int(result.glucose),
  337. direction: BloodGlucose.Direction(from: result.direction ?? ""),
  338. date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
  339. dateString: result.date ?? Date(),
  340. unfiltered: Decimal(result.glucose),
  341. filtered: Decimal(result.glucose),
  342. noise: nil,
  343. glucose: Int(result.glucose)
  344. )
  345. // NightscoutTreatment(
  346. // duration: nil,
  347. // rawDuration: nil,
  348. // rawRate: nil,
  349. // absolute: nil,
  350. // rate: nil,
  351. // eventType: .capillaryGlucose,
  352. // createdAt: result.date,
  353. // enteredBy: "Trio",
  354. // bolus: nil,
  355. // insulin: nil,
  356. // notes: "Trio User",
  357. // carbs: nil,
  358. // fat: nil,
  359. // protein: nil,
  360. // foodType: nil,
  361. // targetTop: nil,
  362. // targetBottom: nil,
  363. // glucoseType: "Manual",
  364. // glucose: self.settingsManager.settings
  365. // .units == .mgdL ? (self.glucoseFormatter.string(from: Int(result.glucose) as NSNumber) ?? "")
  366. // : (self.glucoseFormatter.string(from: Decimal(result.glucose).asMmolL as NSNumber) ?? ""),
  367. // units: self.settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl",
  368. // id: result.id?.uuidString
  369. // )
  370. }
  371. }
  372. }
  373. var alarm: GlucoseAlarm? {
  374. /// glucose can not be older than 20 minutes due to the predicate in the fetch request
  375. coredataContext.performAndWait {
  376. guard let glucose = fetchLatestGlucose() else { return nil }
  377. let glucoseValue = glucose.glucose
  378. if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
  379. return .low
  380. }
  381. if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
  382. return .high
  383. }
  384. return nil
  385. }
  386. }
  387. }
  388. protocol GlucoseObserver {
  389. func glucoseDidUpdate(_ glucose: [BloodGlucose])
  390. }
  391. enum GlucoseAlarm {
  392. case high
  393. case low
  394. var displayName: String {
  395. switch self {
  396. case .high:
  397. return NSLocalizedString("LOWALERT!", comment: "LOWALERT!")
  398. case .low:
  399. return NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!")
  400. }
  401. }
  402. }